feat(gitea): script d'installation LXC Alpine + README #26
@@ -22,6 +22,7 @@ These scripts automate the deployment of personal infrastructure components. The
|
|||||||
| [`proxy/install.sh`](proxy/) | Reverse proxy with Tailscale + Nginx Proxy Manager | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh` \| `bash` |
|
| [`proxy/install.sh`](proxy/) | Reverse proxy with Tailscale + Nginx Proxy Manager | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh` \| `bash` |
|
||||||
| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` |
|
| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` |
|
||||||
| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` |
|
| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` |
|
||||||
|
| [`gitea/install.sh`](gitea/) | Gitea Git service on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh)"` |
|
||||||
| [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` |
|
| [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` |
|
||||||
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` |
|
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` |
|
||||||
|
|
||||||
|
|||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
# Gitea
|
||||||
|
|
||||||
|
Automated installation and update script for a [Gitea](https://about.gitea.com) instance
|
||||||
|
running inside an Alpine LXC on Proxmox. Replaces a previous deployment via
|
||||||
|
community-scripts. Migrating the existing instance's data is out of scope —
|
||||||
|
this script produces a fresh instance.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
Single script, automatic mode selection:
|
||||||
|
|
||||||
|
| Context | Action |
|
||||||
|
| --------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
|
| From Proxmox host, no existing Gitea container | Detects newest Alpine template, creates LXC, installs Gitea via `apk` |
|
||||||
|
| From Proxmox host, Gitea container already present | Reuses the existing LXC, refreshes packages, reapplies `app.ini` |
|
||||||
|
| From inside an LXC, no `gitea` binary | Installs Gitea from scratch |
|
||||||
|
| From inside an LXC, `gitea` already present | Refreshes packages and reapplies `app.ini` (no data changes) |
|
||||||
|
|
||||||
|
The container is identified by hostname **and** the `gitea` tag, so it is
|
||||||
|
re-found across reruns even if the CTID was auto-allocated the first time.
|
||||||
|
|
||||||
|
### Why `apk`, not a downloaded binary
|
||||||
|
|
||||||
|
Unlike `openbao/install.sh` and `gitea-runner/install.sh`, which fetch a
|
||||||
|
GitHub release, Gitea is installed from the Alpine package repositories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apk add --no-cache gitea gitea-openrc
|
||||||
|
```
|
||||||
|
|
||||||
|
The official binaries on `dl.gitea.com` are glibc/CGO-linked (SQLite via
|
||||||
|
CGO), a bad fit for musl. Alpine packages a native musl build of Gitea in
|
||||||
|
`community`, with a `gitea-openrc` sub-package providing the service. This
|
||||||
|
script re-verifies on every install that `gitea` is available via `community`
|
||||||
|
(not `edge`) on the Alpine release in use, and fails loudly instead of
|
||||||
|
silently pinning `edge/community` if it isn't.
|
||||||
|
|
||||||
|
Consequences for the usual pattern:
|
||||||
|
|
||||||
|
| | openbao / gitea-runner | gitea |
|
||||||
|
| ------- | ------------------------------ | ---------------------------------------- |
|
||||||
|
| Install | curl release from GitHub | `apk add gitea gitea-openrc` |
|
||||||
|
| Service | OpenRC unit written by the script | shipped by the package, used as-is |
|
||||||
|
| Update | swap binary + backup | `refresh_os_packages` (`apk upgrade`) |
|
||||||
|
| Version | tracked in `/opt/*_version.txt` | read from `apk list -I` |
|
||||||
|
|
||||||
|
### Configuration (`app.ini`)
|
||||||
|
|
||||||
|
Managed exclusively via `ini_set` (see `lib/common.sh`, #18) — never a
|
||||||
|
heredoc overwrite. The script owns and merges only the keys listed below;
|
||||||
|
everything else in the Alpine package's default `app.ini` (repository root,
|
||||||
|
session provider, etc.) is left untouched.
|
||||||
|
|
||||||
|
| Section | Key | Value |
|
||||||
|
| ----------- | --------------------------------- | ------------------------------------------------ |
|
||||||
|
| `[server]` | `PROTOCOL` | `http` |
|
||||||
|
| `[server]` | `HTTP_ADDR` / `HTTP_PORT` | `127.0.0.1` / `3000` (loopback; tailscale serve fronts it) |
|
||||||
|
| `[server]` | `DOMAIN` / `ROOT_URL` | `GITEA_DOMAIN` / `GITEA_ROOT_URL` — the **public** URL, not the tailnet one |
|
||||||
|
| `[server]` | `DISABLE_SSH` | `true` (HTTPS-only usage confirmed) |
|
||||||
|
| `[security]`| `INSTALL_LOCK` | `true`, written before the service's first start |
|
||||||
|
| `[security]`| `REVERSE_PROXY_LIMIT` | `GITEA_REVERSE_PROXY_LIMIT` (default `2`) — **validate this, see below** |
|
||||||
|
| `[security]`| `REVERSE_PROXY_TRUSTED_PROXIES` | `GITEA_TRUSTED_PROXIES` — never `*` (CVE-2026-20896) |
|
||||||
|
| `[service]` | `DISABLE_REGISTRATION`, `REQUIRE_CAPTCHA_FOR_LOGIN`, `ENABLE_CAPTCHA` | `true` |
|
||||||
|
| `[log]` | `MODE` / `LEVEL` / `ROOT_PATH` | `file` / `info` / `/var/log/gitea` |
|
||||||
|
| `[log]` | `COLORIZE` | `false` — ANSI codes would break the `<HOST>` match in the fail2ban filter added by #21 |
|
||||||
|
| `[metrics]` | `ENABLED`, `TOKEN`, `ENABLED_ISSUE_BY_REPOSITORY`, `ENABLED_ISSUE_BY_LABEL` | `true` / generated once / `true` / `true` |
|
||||||
|
| `[actions]` | `ENABLED` | `true` |
|
||||||
|
| `[database]`| `DB_TYPE` / `PATH` | `sqlite3` / `/var/lib/gitea/data/gitea.db` |
|
||||||
|
|
||||||
|
The metrics token is generated once (`openssl rand -hex 32`, or supply
|
||||||
|
`GITEA_METRICS_TOKEN`) and never touched again once present — a rerun that
|
||||||
|
regenerated it would silently break the Prometheus scrape config.
|
||||||
|
|
||||||
|
The admin account is created once, non-interactively, after `INSTALL_LOCK`
|
||||||
|
is set and the service has started (Gitea's CLI needs a migrated DB). A
|
||||||
|
rerun skips creation if any admin account already exists, so it never resets
|
||||||
|
an operator-changed password.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
#### Full install (from Proxmox shell)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-running the exact same command later refreshes Alpine packages and
|
||||||
|
reapplies `app.ini`, without touching the SQLite data or regenerating the
|
||||||
|
metrics token / admin account.
|
||||||
|
|
||||||
|
#### Customisation
|
||||||
|
|
||||||
|
Every parameter is exposed as an environment variable:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ------------------------------ | ----------------------------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) |
|
||||||
|
| `GITEA_HOSTNAME` | `gitea` | LXC hostname (also used as the tailnet MagicDNS name) |
|
||||||
|
| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` |
|
||||||
|
| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk |
|
||||||
|
| `TEMPLATE_STORAGE` | `local` | Storage where Alpine templates live |
|
||||||
|
| `CORES` | `2` | vCPU cores |
|
||||||
|
| `RAM` | `2048` | RAM in MiB |
|
||||||
|
| `DISK` | `16` | Root disk size in GB |
|
||||||
|
| `BRIDGE` | `vmbr0` | Network bridge |
|
||||||
|
| `LXC_TAG` | `gitea` | Stable tag used to re-discover the container |
|
||||||
|
| `GITEA_DOMAIN` | `gitea.arnodo.fr` | Public domain |
|
||||||
|
| `GITEA_ROOT_URL` | `https://<GITEA_DOMAIN>/` | Public URL (used for clone URLs, webhooks, redirects) |
|
||||||
|
| `GITEA_HTTP_ADDR` / `_PORT` | `127.0.0.1` / `3000` | Local listener; loopback by default |
|
||||||
|
| `GITEA_REVERSE_PROXY_LIMIT` | `2` | Proxy hop count (Traefik + `tailscale serve`) — validate before trusting it |
|
||||||
|
| `GITEA_TRUSTED_PROXIES` | `127.0.0.0/8,::1/128,100.64.0.0/10` | Never set to `*` |
|
||||||
|
| `GITEA_METRICS_TOKEN` | generated | Prometheus bearer token; set once, then immutable |
|
||||||
|
| `GITEA_ADMIN_USER` | `admin` | Admin account username |
|
||||||
|
| `GITEA_ADMIN_EMAIL` | `admin@<GITEA_DOMAIN>` | Admin account email |
|
||||||
|
| `GITEA_ADMIN_PASSWORD` | generated | Admin account password, shown once at install time if generated |
|
||||||
|
| `SYSLOG_TARGET` / `SYSLOG_PORT`| `proxy.taila5ad8.ts.net` / `5514` | Where `gitea.log` is forwarded (proxy's generic rsyslog receiver, #20) |
|
||||||
|
| `TS_AUTHKEY` | _(unset)_ | Pre-auth key; if unset, finish `tailscale up` manually inside the LXC |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CTID=130 GITEA_DOMAIN=git.example.com CORES=4 RAM=4096 \
|
||||||
|
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update (from inside the LXC)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-detected via the presence of the `gitea` binary. Runs
|
||||||
|
`refresh_os_packages` (which brings `gitea`/`gitea-openrc` to the latest
|
||||||
|
available build — no separate `apk upgrade gitea` needed), reapplies
|
||||||
|
`app.ini`, restarts the service, and refreshes the tailnet/rsyslog setup.
|
||||||
|
Re-running from the Proxmox host does the same after refreshing the LXC.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
- **OS**: latest Alpine LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale
|
||||||
|
- **Package**: `gitea` + `gitea-openrc` from Alpine `community`
|
||||||
|
- **Service**: OpenRC, as shipped by the package (`supervise-daemon`), runs as user `gitea`
|
||||||
|
- **Config**: `/etc/gitea/app.ini` — merged via `ini_set`, see table above
|
||||||
|
- **Data**: `/var/lib/gitea` (SQLite DB, repositories, LFS)
|
||||||
|
- **Logs**: `/var/log/gitea/gitea.log` (Gitea's structured log, rotated daily/7d) forwarded via rsyslog to the proxy's receiver (#20); `/var/log/gitea/http.log` is the raw process stdout/stderr capture
|
||||||
|
- **Network**: listener bound to `127.0.0.1:3000`; **Tailscale** runs in the LXC and acts as the reverse proxy (`tailscale serve --https=443`, no port in the resulting URL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment order — non-negotiable
|
||||||
|
|
||||||
|
This issue's script (#19) only produces a working, privately-reachable
|
||||||
|
instance. Bringing it onto the public domain safely requires the rest of the
|
||||||
|
milestone, **in this exact order**:
|
||||||
|
|
||||||
|
```
|
||||||
|
#18 → #19 → validation XFF (ci-dessous) → #20 → #21 → #22
|
||||||
|
```
|
||||||
|
|
||||||
|
> Activer le jail fail2ban (#21) **avant** d'avoir validé la chaîne
|
||||||
|
> `X-Forwarded-For` fait bannir le proxy Traefik lui-même au bout de 5
|
||||||
|
> échecs, et `gitea.arnodo.fr` devient inaccessible dans son intégralité.
|
||||||
|
|
||||||
|
Do not deploy #21 until the procedure below has confirmed `gitea.log`
|
||||||
|
contains the real client IP, not the proxy's tailnet IP.
|
||||||
|
|
||||||
|
## X-Forwarded-For validation procedure
|
||||||
|
|
||||||
|
From an IP known to be external to the tailnet (e.g. mobile hotspot),
|
||||||
|
trigger a failed login against the public URL. Then, inside the LXC:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep "Failed authentication" /var/log/gitea/gitea.log | tail -1
|
||||||
|
```
|
||||||
|
|
||||||
|
| Result | Interpretation | Action |
|
||||||
|
| ------------------ | ------------------------------------------ | ---------------------------------------------------- |
|
||||||
|
| Real public IP | Correct | Proceed to #20 |
|
||||||
|
| `100.x.x.x` | XFF not unwound far enough | Increase `GITEA_REVERSE_PROXY_LIMIT`, rerun the script |
|
||||||
|
| `127.0.0.1` | `tailscale serve` masks everything | Verify `127.0.0.0/8` is in `GITEA_TRUSTED_PROXIES` |
|
||||||
|
|
||||||
|
As long as that line does not show the real client IP, #21 stays
|
||||||
|
undeployed.
|
||||||
Executable
+662
@@ -0,0 +1,662 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# install.sh - Gitea: LXC creation, installation & update
|
||||||
|
# Usage:
|
||||||
|
# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh)"
|
||||||
|
# From inside LXC : bash /root/install.sh (updates packages + app.ini)
|
||||||
|
#
|
||||||
|
# Single entrypoint, three automatic modes:
|
||||||
|
# 1. Proxmox host, no existing container -> create LXC + install Gitea
|
||||||
|
# 2. Proxmox host, container already present -> update packages + app.ini
|
||||||
|
# 3. Inside an LXC -> install if missing, otherwise update
|
||||||
|
#
|
||||||
|
# Installed via `apk add gitea gitea-openrc` rather than the upstream release
|
||||||
|
# binary: dl.gitea.com ships glibc/CGO-linked binaries, a bad fit for musl.
|
||||||
|
# Alpine packages a native musl build in community (verified present on the
|
||||||
|
# 3.22 template as of writing; check_gitea_channel() re-verifies this on every
|
||||||
|
# install rather than trusting that to stay true).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Config (override via environment) ---
|
||||||
|
CTID="${CTID:-}"
|
||||||
|
HOSTNAME_LXC="${GITEA_HOSTNAME:-gitea}"
|
||||||
|
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
|
||||||
|
STORAGE="${STORAGE:-local-lvm}"
|
||||||
|
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
|
||||||
|
CORES="${CORES:-2}"
|
||||||
|
RAM="${RAM:-2048}"
|
||||||
|
DISK="${DISK:-16}"
|
||||||
|
BRIDGE="${BRIDGE:-vmbr0}"
|
||||||
|
LXC_TAG="${LXC_TAG:-gitea}" # stable identifier for the container
|
||||||
|
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
|
||||||
|
# testing from a non-main branch, e.g.
|
||||||
|
# SCRIPT_URL="https://gitea.arnodo.fr/.../branch/feat/gitea-lxc/gitea/install.sh"
|
||||||
|
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea/install.sh}"
|
||||||
|
# Optional: pre-authorise the LXC's Tailscale non-interactively.
|
||||||
|
# Generate at https://login.tailscale.com/admin/settings/keys
|
||||||
|
TS_AUTHKEY="${TS_AUTHKEY:-}"
|
||||||
|
|
||||||
|
# --- app.ini (managed exclusively via ini_set, see configure_app_ini) ---
|
||||||
|
GITEA_DOMAIN="${GITEA_DOMAIN:-gitea.arnodo.fr}" # public domain
|
||||||
|
GITEA_ROOT_URL="${GITEA_ROOT_URL:-https://${GITEA_DOMAIN}/}" # public URL, NOT the tailnet URL
|
||||||
|
GITEA_HTTP_ADDR="${GITEA_HTTP_ADDR:-127.0.0.1}" # loopback; tailscale serve fronts it
|
||||||
|
GITEA_HTTP_PORT="${GITEA_HTTP_PORT:-3000}"
|
||||||
|
# Hop count between the client and this instance. Traefik + `tailscale serve`
|
||||||
|
# is the assumed chain (2 hops) — validate empirically per gitea/README.md
|
||||||
|
# before relying on the resulting client IP for the fail2ban jail in #21.
|
||||||
|
GITEA_REVERSE_PROXY_LIMIT="${GITEA_REVERSE_PROXY_LIMIT:-2}"
|
||||||
|
# Must never be "*" (CVE-2026-20896). 100.64.0.0/10 is Tailscale's CGNAT range.
|
||||||
|
GITEA_TRUSTED_PROXIES="${GITEA_TRUSTED_PROXIES:-127.0.0.0/8,::1/128,100.64.0.0/10}"
|
||||||
|
# Prometheus scrape token. Generated on first install if left unset; once set
|
||||||
|
# in app.ini it is never regenerated (see setup_metrics_token).
|
||||||
|
GITEA_METRICS_TOKEN="${GITEA_METRICS_TOKEN:-}"
|
||||||
|
|
||||||
|
# --- Admin account (created once; see create_admin_user) ---
|
||||||
|
GITEA_ADMIN_USER="${GITEA_ADMIN_USER:-admin}"
|
||||||
|
GITEA_ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-admin@${GITEA_DOMAIN}}"
|
||||||
|
GITEA_ADMIN_PASSWORD="${GITEA_ADMIN_PASSWORD:-}" # generated if unset
|
||||||
|
|
||||||
|
# --- rsyslog forwarding to the proxy's receiver (see proxy #20) ---
|
||||||
|
SYSLOG_TARGET="${SYSLOG_TARGET:-proxy.taila5ad8.ts.net}"
|
||||||
|
SYSLOG_PORT="${SYSLOG_PORT:-5514}"
|
||||||
|
|
||||||
|
# --- Fixed paths (Alpine package layout; not meant to be overridden) ---
|
||||||
|
APP_INI="/etc/gitea/app.ini"
|
||||||
|
GITEA_WORK_DIR="/var/lib/gitea"
|
||||||
|
GITEA_LOG_DIR="/var/log/gitea"
|
||||||
|
|
||||||
|
# --- Colors ---
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Load shared helpers (lib/common.sh: detect_latest_alpine_template,
|
||||||
|
# enable_tty1_autologin, find_existing_lxc, refresh_os_packages, ini_set).
|
||||||
|
#
|
||||||
|
# Same reasoning as openbao/install.sh and gitea-runner/install.sh: a local
|
||||||
|
# checkout has the file on disk right next to us, but the documented curl
|
||||||
|
# one-liner (host or piped into `pct exec` inside the LXC) has no
|
||||||
|
# BASH_SOURCE path worth trusting, so fall back to fetching lib/common.sh
|
||||||
|
# over HTTP next to SCRIPT_URL. The LXC already needs outbound network to
|
||||||
|
# curl this very script and to apk-install gitea, so this adds no new
|
||||||
|
# failure mode.
|
||||||
|
# ============================================================
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)"
|
||||||
|
LIB_COMMON_URL="$(dirname "$(dirname "$SCRIPT_URL")")/lib/common.sh"
|
||||||
|
if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/../lib/common.sh" ]]; then
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
else
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source <(curl -fsSL "$LIB_COMMON_URL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `source <(curl ...)` swallows curl failures: an empty stream still makes
|
||||||
|
# `source` return 0, so a 404/network error would otherwise only surface
|
||||||
|
# later as a confusing "command not found" for ini_set et al. Fail loudly
|
||||||
|
# here instead, with the URL that was tried.
|
||||||
|
if ! declare -F ini_set >/dev/null; then
|
||||||
|
log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
log_error "This script must be run as root (current uid: $(id -u))."
|
||||||
|
log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Refuse to install from edge/community: acceptable for a throwaway test
|
||||||
|
# box, not for a production instance. Re-checked on every install rather
|
||||||
|
# than assumed, since the issue this script implements only verified this
|
||||||
|
# against Alpine 3.22 at write time.
|
||||||
|
# ============================================================
|
||||||
|
check_gitea_channel() {
|
||||||
|
local repo_line
|
||||||
|
repo_line=$(apk policy gitea 2>/dev/null | awk '/^[[:space:]]+https?:\/\//{print; exit}')
|
||||||
|
|
||||||
|
if [[ -z "$repo_line" ]]; then
|
||||||
|
log_error "Package 'gitea' not found in the configured Alpine repositories."
|
||||||
|
log_error "This Alpine release may not package Gitea — see https://pkgs.alpinelinux.org/packages?name=gitea"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$repo_line" == *"/edge/"* ]]; then
|
||||||
|
log_error "gitea is only available via edge/community on this Alpine release."
|
||||||
|
log_error "Refusing to install from edge on what should be a production instance."
|
||||||
|
log_error "Pin an Alpine template where gitea has reached a stable release (TEMPLATE=...),"
|
||||||
|
log_error "or comment on issue #19 with what you found so the assumption can be revisited."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "gitea package available via:${repo_line}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Merge app.ini to the keys this script owns via ini_set (#18). Never a
|
||||||
|
# heredoc overwrite: the file ships with sane Alpine-package defaults for
|
||||||
|
# everything we don't list here, and a rejoué script must only touch its
|
||||||
|
# own keys (see lib/common.sh's ini_set contract).
|
||||||
|
# ============================================================
|
||||||
|
configure_app_ini() {
|
||||||
|
log_info "Configuring ${APP_INI}..."
|
||||||
|
|
||||||
|
ini_set "$APP_INI" server PROTOCOL http
|
||||||
|
ini_set "$APP_INI" server HTTP_ADDR "$GITEA_HTTP_ADDR"
|
||||||
|
ini_set "$APP_INI" server HTTP_PORT "$GITEA_HTTP_PORT"
|
||||||
|
ini_set "$APP_INI" server DOMAIN "$GITEA_DOMAIN"
|
||||||
|
ini_set "$APP_INI" server ROOT_URL "$GITEA_ROOT_URL"
|
||||||
|
ini_set "$APP_INI" server DISABLE_SSH true
|
||||||
|
|
||||||
|
# INSTALL_LOCK must land before the service's first start, or the web
|
||||||
|
# installer is exposed on the public domain and the first visitor becomes
|
||||||
|
# admin. configure_app_ini() always runs before rc-service gitea start in
|
||||||
|
# install_inside_lxc — do not reorder that.
|
||||||
|
ini_set "$APP_INI" security INSTALL_LOCK true
|
||||||
|
ini_set "$APP_INI" security REVERSE_PROXY_LIMIT "$GITEA_REVERSE_PROXY_LIMIT"
|
||||||
|
ini_set "$APP_INI" security REVERSE_PROXY_TRUSTED_PROXIES "$GITEA_TRUSTED_PROXIES"
|
||||||
|
|
||||||
|
ini_set "$APP_INI" service DISABLE_REGISTRATION true
|
||||||
|
ini_set "$APP_INI" service REQUIRE_CAPTCHA_FOR_LOGIN true
|
||||||
|
ini_set "$APP_INI" service ENABLE_CAPTCHA true
|
||||||
|
|
||||||
|
ini_set "$APP_INI" log MODE file
|
||||||
|
ini_set "$APP_INI" log LEVEL info
|
||||||
|
ini_set "$APP_INI" log ROOT_PATH "$GITEA_LOG_DIR"
|
||||||
|
# ANSI color codes in gitea.log would break the <HOST> match in the
|
||||||
|
# fail2ban filter added by #21.
|
||||||
|
ini_set "$APP_INI" log COLORIZE false
|
||||||
|
|
||||||
|
ini_set "$APP_INI" actions ENABLED true
|
||||||
|
|
||||||
|
ini_set "$APP_INI" database DB_TYPE sqlite3
|
||||||
|
ini_set "$APP_INI" database PATH "${GITEA_WORK_DIR}/data/gitea.db"
|
||||||
|
|
||||||
|
setup_metrics_token
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Metrics token: generate once, then leave alone. A rejeu that regenerated
|
||||||
|
# it would silently break the Prometheus scrape config.
|
||||||
|
# ============================================================
|
||||||
|
setup_metrics_token() {
|
||||||
|
local current_token
|
||||||
|
current_token=$(awk '
|
||||||
|
/^\[metrics\]/ { insec = 1; next }
|
||||||
|
/^\[/ { insec = 0 }
|
||||||
|
insec && match($0, /^[ \t]*TOKEN[ \t]*=/) {
|
||||||
|
sub(/^[^=]*=[ \t]*/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$APP_INI" 2>/dev/null || true)
|
||||||
|
|
||||||
|
local token="${current_token:-${GITEA_METRICS_TOKEN}}"
|
||||||
|
if [[ -z "$token" ]]; then
|
||||||
|
token=$(openssl rand -hex 32)
|
||||||
|
log_info "Generated new Prometheus metrics token (shown once, save it now):"
|
||||||
|
log_info " ${token}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ini_set "$APP_INI" metrics ENABLED true
|
||||||
|
ini_set "$APP_INI" metrics TOKEN "$token"
|
||||||
|
ini_set "$APP_INI" metrics ENABLED_ISSUE_BY_REPOSITORY true
|
||||||
|
ini_set "$APP_INI" metrics ENABLED_ISSUE_BY_LABEL true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Block until Gitea answers its health endpoint. Both create_admin_user()
|
||||||
|
# (DB must be migrated) and configure_rsyslog_forwarder() (log file must
|
||||||
|
# exist) depend on the service actually being up.
|
||||||
|
# ============================================================
|
||||||
|
wait_for_gitea_ready() {
|
||||||
|
local tries=0
|
||||||
|
until curl -fsS "http://${GITEA_HTTP_ADDR}:${GITEA_HTTP_PORT}/api/healthz" >/dev/null 2>&1; do
|
||||||
|
tries=$((tries + 1))
|
||||||
|
if (( tries > 30 )); then
|
||||||
|
log_error "Gitea did not become healthy within 30s."
|
||||||
|
log_error "Check: rc-service gitea status && tail -50 ${GITEA_LOG_DIR}/gitea.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Idempotent admin creation: the DB only exists once Gitea has started at
|
||||||
|
# least once (its CLI does not migrate on its own), so this must run after
|
||||||
|
# wait_for_gitea_ready(). Skips creation if any admin already exists,
|
||||||
|
# regardless of username, so a rerun never touches an operator-renamed or
|
||||||
|
# operator-created admin account.
|
||||||
|
# ============================================================
|
||||||
|
create_admin_user() {
|
||||||
|
local existing
|
||||||
|
existing=$(su -s /bin/sh gitea -c \
|
||||||
|
"gitea admin user list --admin --config '${APP_INI}' --work-path '${GITEA_WORK_DIR}'" \
|
||||||
|
2>/dev/null | tail -n +2 | grep -c . || true)
|
||||||
|
|
||||||
|
if [[ "${existing:-0}" -gt 0 ]]; then
|
||||||
|
log_info "Admin account already present, skipping creation."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local password="${GITEA_ADMIN_PASSWORD:-$(openssl rand -base64 24)}"
|
||||||
|
log_info "Creating admin account '${GITEA_ADMIN_USER}'..."
|
||||||
|
su -s /bin/sh gitea -c \
|
||||||
|
"gitea admin user create --admin --username '${GITEA_ADMIN_USER}' --email '${GITEA_ADMIN_EMAIL}' --password '${password}' --must-change-password=true --config '${APP_INI}' --work-path '${GITEA_WORK_DIR}'"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Admin account created (shown once, save it now):"
|
||||||
|
log_info " Username: ${GITEA_ADMIN_USER}"
|
||||||
|
log_info " Password: ${password}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Forward gitea.log to the proxy's generic rsyslog receiver (#20), tagged
|
||||||
|
# "gitea" so the proxy can route it into its own file (#21) for a
|
||||||
|
# proxy-side fail2ban jail — a jail running in this LXC would only ever
|
||||||
|
# see the proxy's own tailnet IP as the source and end up banning the
|
||||||
|
# proxy. Scoped to $programname == "gitea" so the LXC's own local syslog
|
||||||
|
# traffic (cron, auth, rsyslog's startup messages) is never forwarded.
|
||||||
|
# ============================================================
|
||||||
|
configure_rsyslog_forwarder() {
|
||||||
|
log_info "Configuring rsyslog forwarding to ${SYSLOG_TARGET}:${SYSLOG_PORT}..."
|
||||||
|
apk add --no-cache rsyslog >/dev/null
|
||||||
|
mkdir -p /etc/rsyslog.d
|
||||||
|
|
||||||
|
cat > /etc/rsyslog.d/50-gitea-forward.conf << EOF
|
||||||
|
module(load="imfile")
|
||||||
|
|
||||||
|
input(type="imfile"
|
||||||
|
File="${GITEA_LOG_DIR}/gitea.log"
|
||||||
|
Tag="gitea"
|
||||||
|
Severity="info"
|
||||||
|
Facility="local0")
|
||||||
|
|
||||||
|
if \$programname == "gitea" then {
|
||||||
|
action(type="omfwd" target="${SYSLOG_TARGET}" port="${SYSLOG_PORT}" protocol="tcp")
|
||||||
|
stop
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rc-update add rsyslog default >/dev/null 2>&1 || true
|
||||||
|
rc-service rsyslog status >/dev/null 2>&1 && rc-service rsyslog stop
|
||||||
|
|
||||||
|
# SYSLOG_TARGET is a MagicDNS name; omfwd resolves it once at rsyslog
|
||||||
|
# startup, so starting before Tailscale has come up and propagated the
|
||||||
|
# name would leave the forwarder silently broken until the next restart.
|
||||||
|
# Same bounded-wait pattern as gitea-runner/install.sh's start_pre().
|
||||||
|
# configure_tailscale_proxy() must already have run by the time we get
|
||||||
|
# here — do not reorder that.
|
||||||
|
local tries=0
|
||||||
|
until getent hosts "$SYSLOG_TARGET" >/dev/null 2>&1; do
|
||||||
|
tries=$((tries + 1))
|
||||||
|
if (( tries > 30 )); then
|
||||||
|
log_warn "Could not resolve ${SYSLOG_TARGET} after 30s — starting rsyslog anyway."
|
||||||
|
log_warn "Forwarding will stay broken until the name resolves and rsyslog is restarted (rerun this script)."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
rc-service rsyslog start
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Reusable: bring Tailscale up and publish Gitea on the tailnet.
|
||||||
|
# Idempotent: re-running is a no-op once Tailscale is logged in and the
|
||||||
|
# serve mapping is already in place. Mirrors openbao/install.sh's helper
|
||||||
|
# of the same name.
|
||||||
|
# ============================================================
|
||||||
|
configure_tailscale_proxy() {
|
||||||
|
if ! command -v tailscale >/dev/null 2>&1; then
|
||||||
|
log_warn "tailscale CLI not found, skipping reverse-proxy setup."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local backend_state
|
||||||
|
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
|
||||||
|
if [[ "$backend_state" != "Running" ]]; then
|
||||||
|
if [[ -n "$TS_AUTHKEY" ]]; then
|
||||||
|
log_info "Bringing Tailscale up with provided auth key..."
|
||||||
|
tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$HOSTNAME_LXC" \
|
||||||
|
|| log_warn "tailscale up failed — run it manually inside the LXC."
|
||||||
|
else
|
||||||
|
log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied."
|
||||||
|
log_warn "Finish setup inside the LXC with: tailscale up --ssh"
|
||||||
|
log_warn "Then publish Gitea with: tailscale serve --bg --https=443 http://${GITEA_HTTP_ADDR}:${GITEA_HTTP_PORT}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tailscale serve status 2>/dev/null | grep -q "${GITEA_HTTP_ADDR}:${GITEA_HTTP_PORT}"; then
|
||||||
|
log_info "Tailscale serve already publishes http://${GITEA_HTTP_ADDR}:${GITEA_HTTP_PORT}."
|
||||||
|
else
|
||||||
|
log_info "Publishing Gitea on the tailnet via 'tailscale serve' (HTTPS:443)..."
|
||||||
|
tailscale serve --bg --https=443 "http://${GITEA_HTTP_ADDR}:${GITEA_HTTP_PORT}" \
|
||||||
|
|| log_warn "tailscale serve failed — enable HTTPS on your tailnet and retry."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local fqdn
|
||||||
|
fqdn=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // ""' | sed 's/\.$//')
|
||||||
|
if [[ -n "$fqdn" ]]; then
|
||||||
|
log_info "Gitea reachable on the tailnet at: https://${fqdn}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Proxmox-host helpers
|
||||||
|
# ============================================================
|
||||||
|
allocate_ctid() {
|
||||||
|
pvesh get /cluster/nextid 2>/dev/null \
|
||||||
|
|| pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||||
|
| jq '[.[].vmid] | max + 1' \
|
||||||
|
|| echo 100
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inject the script into the container and execute it in the requested mode.
|
||||||
|
# Forwards the runtime configuration the inner invocation needs to reproduce
|
||||||
|
# what the user requested on the host (mirrors openbao/gitea-runner's
|
||||||
|
# exec_in_lxc).
|
||||||
|
exec_in_lxc() {
|
||||||
|
local ctid="$1"
|
||||||
|
local mode="$2" # --install or --update
|
||||||
|
|
||||||
|
pct exec "$ctid" -- sh -c "apk add --no-cache bash curl jq ca-certificates >/dev/null 2>&1"
|
||||||
|
curl -fsSL "$SCRIPT_URL" \
|
||||||
|
| pct exec "$ctid" -- env \
|
||||||
|
SCRIPT_URL="$SCRIPT_URL" \
|
||||||
|
GITEA_HOSTNAME="$HOSTNAME_LXC" \
|
||||||
|
GITEA_DOMAIN="$GITEA_DOMAIN" \
|
||||||
|
GITEA_ROOT_URL="$GITEA_ROOT_URL" \
|
||||||
|
GITEA_HTTP_ADDR="$GITEA_HTTP_ADDR" \
|
||||||
|
GITEA_HTTP_PORT="$GITEA_HTTP_PORT" \
|
||||||
|
GITEA_REVERSE_PROXY_LIMIT="$GITEA_REVERSE_PROXY_LIMIT" \
|
||||||
|
GITEA_TRUSTED_PROXIES="$GITEA_TRUSTED_PROXIES" \
|
||||||
|
GITEA_METRICS_TOKEN="$GITEA_METRICS_TOKEN" \
|
||||||
|
GITEA_ADMIN_USER="$GITEA_ADMIN_USER" \
|
||||||
|
GITEA_ADMIN_EMAIL="$GITEA_ADMIN_EMAIL" \
|
||||||
|
GITEA_ADMIN_PASSWORD="$GITEA_ADMIN_PASSWORD" \
|
||||||
|
SYSLOG_TARGET="$SYSLOG_TARGET" \
|
||||||
|
SYSLOG_PORT="$SYSLOG_PORT" \
|
||||||
|
TS_AUTHKEY="$TS_AUTHKEY" \
|
||||||
|
bash -s -- "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MODE: Proxmox host — create LXC + install
|
||||||
|
# ============================================================
|
||||||
|
create_lxc() {
|
||||||
|
log_info "=== Gitea — LXC creation ==="
|
||||||
|
|
||||||
|
if [[ -z "$TEMPLATE" ]]; then
|
||||||
|
TEMPLATE=$(detect_latest_alpine_template)
|
||||||
|
else
|
||||||
|
log_info "Using user-provided template: $TEMPLATE"
|
||||||
|
fi
|
||||||
|
ensure_template_present "$TEMPLATE"
|
||||||
|
|
||||||
|
if [[ -z "$CTID" ]]; then
|
||||||
|
CTID=$(allocate_ctid)
|
||||||
|
log_info "Auto-selected CTID: $CTID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Creating LXC ${CTID} (${HOSTNAME_LXC})..."
|
||||||
|
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||||
|
--hostname "$HOSTNAME_LXC" \
|
||||||
|
--cores "$CORES" \
|
||||||
|
--memory "$RAM" \
|
||||||
|
--rootfs "${STORAGE}:${DISK}" \
|
||||||
|
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
|
||||||
|
--unprivileged 1 \
|
||||||
|
--features "nesting=1" \
|
||||||
|
--tags "infra-script,${LXC_TAG}" \
|
||||||
|
--onboot 1 \
|
||||||
|
--start 0
|
||||||
|
|
||||||
|
# Tailscale needs /dev/net/tun inside the unprivileged container.
|
||||||
|
log_info "Adding /dev/net/tun passthrough for Tailscale..."
|
||||||
|
cat >> "/etc/pve/lxc/${CTID}.conf" <<EOF
|
||||||
|
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||||
|
lxc.mount.entry: /dev/net dev/net none bind,create=dir
|
||||||
|
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_info "Starting LXC ${CTID}..."
|
||||||
|
pct start "$CTID"
|
||||||
|
local tries=0
|
||||||
|
until pct exec "$CTID" -- sh -c "ip -4 addr show eth0 | grep -q 'inet '" 2>/dev/null; do
|
||||||
|
tries=$((tries + 1))
|
||||||
|
if (( tries > 20 )); then
|
||||||
|
log_error "LXC ${CTID} did not acquire an IP after 20s."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Running installer inside LXC ${CTID}..."
|
||||||
|
exec_in_lxc "$CTID" "--install"
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "========================================="
|
||||||
|
log_info "LXC ${CTID} created successfully!"
|
||||||
|
log_info "========================================="
|
||||||
|
echo ""
|
||||||
|
echo " Hostname : ${HOSTNAME_LXC}"
|
||||||
|
echo " IP : ${ip:-pending}"
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT — do not deploy the fail2ban jail (#21) yet. Validate the"
|
||||||
|
echo "X-Forwarded-For chain first (see gitea/README.md):"
|
||||||
|
echo " 1. Trigger a failed login from a known external IP."
|
||||||
|
echo " 2. pct enter ${CTID} && grep 'Failed authentication' ${GITEA_LOG_DIR}/gitea.log | tail -1"
|
||||||
|
echo " 3. Only once that line shows the real client IP, deploy #20 then #21."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MODE: Proxmox host — update existing LXC
|
||||||
|
# ============================================================
|
||||||
|
update_lxc() {
|
||||||
|
local ctid="$1"
|
||||||
|
log_info "=== Gitea — updating existing LXC ${ctid} ==="
|
||||||
|
|
||||||
|
if ! pct status "$ctid" | grep -q running; then
|
||||||
|
log_info "Starting LXC ${ctid}..."
|
||||||
|
pct start "$ctid"
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Updating LXC ${ctid}..."
|
||||||
|
exec_in_lxc "$ctid" "--update"
|
||||||
|
|
||||||
|
log_info "Update of LXC ${ctid} complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MODE: inside LXC — fresh install
|
||||||
|
# ============================================================
|
||||||
|
install_inside_lxc() {
|
||||||
|
log_info "=== Gitea — installation ==="
|
||||||
|
|
||||||
|
log_info "Updating package index..."
|
||||||
|
apk update >/dev/null
|
||||||
|
|
||||||
|
check_gitea_channel
|
||||||
|
|
||||||
|
log_info "Installing dependencies..."
|
||||||
|
apk add --no-cache bash curl jq ca-certificates openssl openrc tailscale >/dev/null
|
||||||
|
|
||||||
|
log_info "Installing gitea + gitea-openrc..."
|
||||||
|
apk add --no-cache gitea gitea-openrc >/dev/null
|
||||||
|
|
||||||
|
log_info "Enabling tailscaled..."
|
||||||
|
rc-update add tailscale default >/dev/null 2>&1 || true
|
||||||
|
rc-service tailscale start >/dev/null 2>&1 || log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)"
|
||||||
|
|
||||||
|
configure_app_ini
|
||||||
|
|
||||||
|
log_info "Starting gitea service..."
|
||||||
|
rc-update add gitea default >/dev/null 2>&1 || true
|
||||||
|
rc-service gitea status >/dev/null 2>&1 && rc-service gitea stop
|
||||||
|
rc-service gitea start
|
||||||
|
|
||||||
|
wait_for_gitea_ready
|
||||||
|
create_admin_user
|
||||||
|
|
||||||
|
configure_tailscale_proxy
|
||||||
|
|
||||||
|
# SYSLOG_TARGET is a MagicDNS name — must run after configure_tailscale_proxy
|
||||||
|
# so the tailnet (and MagicDNS) is actually up by the time rsyslog starts.
|
||||||
|
configure_rsyslog_forwarder
|
||||||
|
|
||||||
|
log_info "Configuring logrotate for ${GITEA_LOG_DIR}/gitea.log..."
|
||||||
|
apk add --no-cache logrotate >/dev/null
|
||||||
|
cat > /etc/logrotate.d/gitea <<EOF
|
||||||
|
${GITEA_LOG_DIR}/gitea.log {
|
||||||
|
daily
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
|
||||||
|
|
||||||
|
enable_tty1_autologin
|
||||||
|
|
||||||
|
log_info "Configuring MOTD..."
|
||||||
|
# /etc/profile.d/ runs for every interactive login shell — works for both
|
||||||
|
# the auto-login tty and Tailscale SSH. Quoted heredoc: every variable is
|
||||||
|
# resolved at login time, not at install time; the two __GITEA_*__
|
||||||
|
# markers below are the only install-time values, substituted after the fact.
|
||||||
|
cat > /etc/profile.d/00-gitea.sh <<'MOTD'
|
||||||
|
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
|
||||||
|
/"Self"/ { in_self=1 }
|
||||||
|
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
|
||||||
|
')
|
||||||
|
[[ -z "$TS_FQDN" ]] && TS_FQDN="$(hostname).ts.net"
|
||||||
|
|
||||||
|
GITEA_VERSION=$(apk info -e -v gitea 2>/dev/null | sed 's/^gitea-//')
|
||||||
|
[[ -z "$GITEA_VERSION" ]] && GITEA_VERSION="unknown"
|
||||||
|
|
||||||
|
if rc-service gitea status >/dev/null 2>&1; then
|
||||||
|
SVC_STATE="running"
|
||||||
|
else
|
||||||
|
SVC_STATE="stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ____ _ _ "
|
||||||
|
echo "/ ___(_) |_ ___ __ _ "
|
||||||
|
echo "| | _| | __/ _ \/ _\` |"
|
||||||
|
echo "| |_| | | || __/ (_| |"
|
||||||
|
echo "\____|_|\__\___|\__,_|"
|
||||||
|
echo ""
|
||||||
|
echo "Gitea (${GITEA_VERSION})"
|
||||||
|
echo "─────────────────────────────────────────"
|
||||||
|
echo "Access:"
|
||||||
|
echo " • Tailnet : https://${TS_FQDN}"
|
||||||
|
echo " • Public : __GITEA_ROOT_URL__"
|
||||||
|
echo " • Service : ${SVC_STATE}"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " rc-service gitea status"
|
||||||
|
echo " tail -f __GITEA_LOG_DIR__/gitea.log"
|
||||||
|
echo "─────────────────────────────────────────"
|
||||||
|
echo ""
|
||||||
|
MOTD
|
||||||
|
sed -i "s#__GITEA_ROOT_URL__#${GITEA_ROOT_URL}#; s#__GITEA_LOG_DIR__#${GITEA_LOG_DIR}#" /etc/profile.d/00-gitea.sh
|
||||||
|
chmod +x /etc/profile.d/00-gitea.sh
|
||||||
|
|
||||||
|
log_info "Cleaning up..."
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "========================================="
|
||||||
|
log_info "Gitea installation complete!"
|
||||||
|
log_info "========================================="
|
||||||
|
echo ""
|
||||||
|
echo " Public URL : ${GITEA_ROOT_URL}"
|
||||||
|
echo ""
|
||||||
|
echo "IMPORTANT — do not deploy the fail2ban jail (#21) yet. Validate the"
|
||||||
|
echo "X-Forwarded-For chain first (see gitea/README.md):"
|
||||||
|
echo " grep 'Failed authentication' ${GITEA_LOG_DIR}/gitea.log | tail -1"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MODE: inside LXC — update only
|
||||||
|
# ============================================================
|
||||||
|
update_inside_lxc() {
|
||||||
|
log_info "=== Gitea — update ==="
|
||||||
|
|
||||||
|
# refresh_os_packages() runs an unscoped `apk update && apk upgrade`,
|
||||||
|
# which already brings gitea/gitea-openrc to the latest available build —
|
||||||
|
# no separate `apk upgrade gitea` call needed.
|
||||||
|
refresh_os_packages
|
||||||
|
|
||||||
|
configure_app_ini
|
||||||
|
|
||||||
|
rc-service gitea status >/dev/null 2>&1 && rc-service gitea stop
|
||||||
|
rc-service gitea start
|
||||||
|
wait_for_gitea_ready
|
||||||
|
|
||||||
|
configure_tailscale_proxy
|
||||||
|
# SYSLOG_TARGET is a MagicDNS name — must run after configure_tailscale_proxy
|
||||||
|
# so the tailnet (and MagicDNS) is actually up by the time rsyslog starts.
|
||||||
|
configure_rsyslog_forwarder
|
||||||
|
|
||||||
|
log_info "Gitea version: $(apk info -e -v gitea 2>/dev/null)"
|
||||||
|
log_info "Update complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Main — dispatch on explicit mode flag or auto-detect context
|
||||||
|
# ============================================================
|
||||||
|
main() {
|
||||||
|
case "${1:-}" in
|
||||||
|
--install)
|
||||||
|
install_inside_lxc
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--update)
|
||||||
|
update_inside_lxc
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if command -v pct >/dev/null 2>&1; then
|
||||||
|
# Running on a Proxmox host
|
||||||
|
require_root
|
||||||
|
|
||||||
|
local existing=""
|
||||||
|
if existing=$(find_existing_lxc); then
|
||||||
|
log_info "Found existing Gitea LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
|
||||||
|
update_lxc "$existing"
|
||||||
|
else
|
||||||
|
create_lxc
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Inside a container (no Proxmox tooling)
|
||||||
|
require_root
|
||||||
|
if command -v gitea >/dev/null 2>&1; then
|
||||||
|
update_inside_lxc
|
||||||
|
else
|
||||||
|
install_inside_lxc
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user