Files
infra-scripts/gitea/install.sh
T
Damien f71abf9b23 feat(gitea): add LXC install script + README
New gitea/install.sh, modeled on openbao/install.sh's single-entrypoint
three-mode pattern. Installs via `apk add gitea gitea-openrc` rather
than a downloaded release binary: dl.gitea.com ships glibc/CGO-linked
binaries, a bad fit for musl. check_gitea_channel() re-verifies on
every install that the package is available via community (not edge)
rather than trusting that to stay true.

app.ini is owned exclusively by ini_set (#18) — never a heredoc
overwrite — so a rejoué script adds newly-required keys without
clobbering operator changes elsewhere in the file. INSTALL_LOCK is
set before the service's first start so the web installer is never
exposed. The Prometheus metrics token and admin account are each
created once and left alone on reruns. gitea.log is forwarded to the
proxy's rsyslog receiver (#20) tagged "gitea", scoped so the LXC's own
local syslog traffic is never forwarded — a jail running in this LXC
would only ever see the proxy's tailnet IP and end up banning the
proxy itself.

Root README's script table gets a line for the new script.

Refs #19
2026-07-31 19:19:33 +02:00

641 lines
24 KiB
Bash
Executable File

#!/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
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 gcompat 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_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
configure_tailscale_proxy
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 list -I 2>/dev/null | awk '/^gitea-[0-9]/{print $1; exit}' | 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_rsyslog_forwarder
configure_tailscale_proxy
log_info "Gitea version: $(apk list -I 2>/dev/null | awk '/^gitea-[0-9]/{print $1; exit}')"
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 "$@"