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` |
|
||||
| [`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/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)"` |
|
||||
| [`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