Standardiser les scripts créateurs de LXC (template, autologin, update) #16

Merged
Damien merged 10 commits from chore/standardize-lxc-scripts into main 2026-07-30 12:19:38 +00:00
8 changed files with 297 additions and 929 deletions
+3 -4
View File
@@ -9,7 +9,7 @@ These scripts automate the deployment of personal infrastructure components. The
- **Self-contained**: No external dependencies beyond standard Debian packages - **Self-contained**: No external dependencies beyond standard Debian packages
- **Idempotent-ish**: Safe to re-run (where possible) - **Idempotent-ish**: Safe to re-run (where possible)
- **Curl-friendly**: Designed for one-liner deployment from a fresh server - **Curl-friendly**: Designed for one-liner deployment from a fresh server
- **Multi-OS**: Supports Debian and Alpine-based deployments - **Multi-OS**: Supports Debian and Alpine-based deployments, chosen per-script based on that service's requirements
- **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination - **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination
- **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files) - **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files)
- **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access) - **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access)
@@ -23,11 +23,10 @@ These scripts automate the deployment of personal infrastructure components. The
| [`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)"` |
| [`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)"` |
| [`ferretdb/install.sh`](ferretdb/) | FerretDB (MongoDB-compatible) on Debian LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/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)"` |
### Requirements ### Requirements
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao, ferretdb) or Alpine VM (komodo) - Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao) or Alpine VM (komodo)
- User with sudo privileges (do not run as root) — except gitea-runner, openbao, ferretdb, and komodo which run as root - User with sudo privileges (do not run as root) — except gitea-runner, openbao, and komodo which run as root
- Internet access - Internet access
-139
View File
@@ -1,139 +0,0 @@
# FerretDB
Automated installation and update script for [FerretDB](https://www.ferretdb.io) — a truly
open-source, MongoDB-compatible database — running inside a **Debian LXC** on Proxmox.
A drop-in MongoDB replacement for any app that speaks the MongoDB wire protocol (e.g.
[LibreChat](https://www.librechat.ai)) without running MongoDB itself. The script installs
and exposes the database only; wiring it into an application is left to that app's own
configuration (a separate script, or manually).
### Why Debian and not Alpine?
FerretDB v2 is two pieces:
1. the **FerretDB proxy** (a static Go binary), and
2. **PostgreSQL + Microsoft's DocumentDB extension**, the mandatory storage engine.
The DocumentDB extension is a compiled C PostgreSQL extension and is published **only** as
`deb`/`rpm` packages (`deb11`, `deb12`, `ubuntu`, `rhel`) — there is **no Alpine/musl build**.
So, unlike the `openbao`/`gitea-runner` Alpine LXCs in this repo, this stack runs on Debian 12
(`deb12`, the newest target the extension ships for).
### Features
Single script, automatic mode selection:
| Context | Action |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| From Proxmox host, no existing FerretDB container | Detects newest Debian template, creates LXC, installs PostgreSQL + DocumentDB + FerretDB |
| From Proxmox host, FerretDB container already present | Reuses the existing LXC, refreshes packages, upgrades the FerretDB stack to latest |
| From inside an LXC, no `ferretdb` binary | Installs the full stack from scratch |
| From inside an LXC, `ferretdb` already present | Updates the packages only (no config / role / data changes) |
The container is identified by hostname **and** the `ferretdb` tag, so it is re-found across
reruns even if the CTID was auto-allocated the first time.
### Requirements
- Proxmox VE host with `pveam`, `pct`, `pvesh`, `jq` available
- Internet access from both the host (template download) and the LXC (package downloads)
- Script must be run as **root** on the Proxmox host (enforced; the Web UI shell qualifies)
### Usage
#### Full install (from Proxmox shell)
```bash
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
```
The script prints the generated password and the ready-to-paste `MONGO_URI` at the end.
**Save them** — the password is not persisted on the Proxmox host.
Re-running the exact same command later upgrades packages inside the LXC and brings the
FerretDB stack to the latest release, without touching the config, role, or PostgreSQL data.
#### Customisation
Every parameter is exposed as an environment variable:
| Variable | Default | Description |
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) |
| `FERRETDB_HOSTNAME` | `ferretdb` | LXC hostname |
| `TEMPLATE` | auto-detected | Debian template; auto-detected from `pveam available` |
| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk |
| `TEMPLATE_STORAGE` | `local` | Storage where Debian templates live |
| `CORES` | `2` | vCPU cores |
| `RAM` | `2048` | RAM in MiB (Postgres + FerretDB) |
| `DISK` | `16` | Root disk size in GB |
| `BRIDGE` | `vmbr0` | Network bridge |
| `LXC_TAG` | `ferretdb` | Stable tag used to re-discover the container |
| `PG_VERSION` | `17` | PostgreSQL major version (from PGDG) |
| `DOCUMENTDB_TAG` | `latest` | DocumentDB release tag (couples documentdb + FerretDB versions); pin e.g. `v0.107.0-ferretdb-2.7.0` |
| `DOCUMENTDB_DISTRO` | `deb12` | Distro target in the documentdb deb filename. Escape hatch for a future `deb13` (set with `TEMPLATE`) |
| `FERRETDB_LISTEN_ADDR` | `0.0.0.0:27017` | TCP listener. Exposed on all interfaces — it is a database other hosts must reach. |
| `FERRETDB_USER` | `ferretdb` | App user — both the PostgreSQL role and the MongoDB user clients authenticate as |
| `FERRETDB_PASSWORD` | auto-generated | Auto-generated (`openssl rand -hex 24`) when unset; printed in the final summary |
| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at <https://login.tailscale.com/admin/settings/keys>). If unset, finish `tailscale up` manually inside the LXC. |
```bash
CTID=220 FERRETDB_HOSTNAME=mongo RAM=4096 DISK=32 \
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
```
#### Connecting a client
Any MongoDB driver or tool connects with a standard connection string (the script prints the
exact one, with the generated password, at the end of the install):
```
mongodb://ferretdb:<password>@<lxc-ip-or-tailnet-fqdn>:27017/
```
Append a database name to target one (e.g. `…:27017/myapp`); it is created on first write.
Wiring this into a specific application (setting its Mongo connection string, disabling any
bundled MongoDB it ships, etc.) is intentionally out of scope — do it from that app's own
config or a dedicated script.
#### Tailscale
The LXC joins the tailnet (`tailscale up --ssh`) so a client on another node can reach the
database over the tailnet. Unlike the `openbao` script there is **no `tailscale serve`** — the
MongoDB wire protocol is raw TCP, not HTTP, so the listener is exposed directly on
`0.0.0.0:27017` (LAN + tailnet) by design.
If `TS_AUTHKEY` was supplied the node is brought up automatically; otherwise finish it
manually inside the LXC:
```bash
pct enter <CTID>
tailscale up --ssh --hostname ferretdb
tailscale status # prints the tailnet FQDN
```
> Because the listener is on `0.0.0.0`, restrict access with your tailnet ACLs and/or a host
> firewall — anyone who can route to TCP 27017 can attempt to authenticate.
#### Update (from inside the LXC)
```bash
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh | bash
```
The script auto-detects the presence of `/usr/bin/ferretdb` and switches to update mode:
packages are upgraded (including the DocumentDB extension via
`ALTER EXTENSION documentdb UPDATE`) and the services are restarted. The config, the
PostgreSQL role, and the data are left untouched.
### Architecture
- **OS**: latest Debian LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale
- **Storage engine**: PostgreSQL `17` (PGDG) + the DocumentDB extension (`pg_documentdb`, `pg_cron`), loopback-only on `127.0.0.1:5432`
- **Proxy**: official `ferretdb` deb from `github.com/FerretDB/FerretDB`, systemd unit, listening on `0.0.0.0:27017`
- **Auth**: the app user is provisioned through `documentdb_api.create_user` (roles `clusterAdmin` + `readWriteAnyDatabase`), **not** a plain `CREATE ROLE`. DocumentDB builds the SCRAM-SHA-256 verifier with its own 28-byte salt (`documentdb.scramDefaultSaltLen`); a native PostgreSQL role would store a 16-byte salt that MongoDB clients reject (`invalid salt length of 16 in sasl step2`). FerretDB connects to PostgreSQL as the same user.
- **Network**: FerretDB exposed on `0.0.0.0:27017`; Tailscale runs in the LXC for tailnet reachability (no `serve`)
- **Config**: `/etc/postgresql/17/main/conf.d/documentdb.conf` (extension settings) and `/etc/systemd/system/ferretdb.service.d/override.conf` (`FERRETDB_POSTGRESQL_URL`, `FERRETDB_LISTEN_ADDR`)
- **Logs**: PostgreSQL via its stock `logrotate`; FerretDB via journald, capped at `SystemMaxUse=200M`
- **Version tracking**: `/opt/ferretdb_version.txt` records the installed DocumentDB tag for idempotent reruns
-676
View File
@@ -1,676 +0,0 @@
#!/bin/bash
# install.sh - FerretDB: LXC creation, installation & update
# Usage:
# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"
# From inside LXC : bash /root/install.sh (updates packages)
#
# Single entrypoint, three automatic modes:
# 1. Proxmox host, no existing container -> create Debian LXC + install FerretDB
# 2. Proxmox host, container already present -> update packages + upgrade FerretDB
# 3. Inside an LXC -> install if missing, otherwise update
#
# FerretDB v2 is a MongoDB wire-protocol proxy backed by PostgreSQL + the
# DocumentDB extension. The extension is a compiled C PostgreSQL extension and
# is only published as deb/rpm packages (no Alpine/musl build), so this stack
# runs on Debian — unlike the openbao/gitea-runner Alpine LXCs in this repo.
#
# The package install/upgrade logic lives in a single reusable function
# (install_or_upgrade_packages) shared by both the create and update paths.
set -euo pipefail
# Force an always-present locale. A fresh Debian template has not generated the
# host's locale (e.g. fr_FR.UTF-8 inherited through pct exec), so apt, perl and
# apt-listchanges warn loudly about it. C.UTF-8 ships with glibc and is always
# valid; this silences the noise without installing extra locales.
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
# --- Config (override via environment) ---
CTID="${CTID:-}"
HOSTNAME_LXC="${FERRETDB_HOSTNAME:-ferretdb}"
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
STORAGE="${STORAGE:-local-lvm}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
CORES="${CORES:-2}"
RAM="${RAM:-2048}" # Postgres needs more headroom than openbao
DISK="${DISK:-16}"
BRIDGE="${BRIDGE:-vmbr0}"
LXC_TAG="${LXC_TAG:-ferretdb}" # stable identifier for the container
PG_VERSION="${PG_VERSION:-17}" # PostgreSQL major version (PGDG)
# DocumentDB release tag couples both pieces: it encodes the documentdb package
# version AND the matching FerretDB version. "latest" resolves both at once.
DOCUMENTDB_TAG="${DOCUMENTDB_TAG:-latest}"
# Distro target embedded in the documentdb deb filename. Only deb11/deb12 exist
# today (no deb13). Escape hatch: when upstream ships deb13, set this + TEMPLATE
# to move to trixie without editing the script.
DOCUMENTDB_DISTRO="${DOCUMENTDB_DISTRO:-deb12}"
DOCUMENTDB_RELEASES_URL="${DOCUMENTDB_RELEASES_URL:-https://api.github.com/repos/FerretDB/documentdb/releases}"
# As a database we deliberately expose the listener on all interfaces so other
# hosts can reach it over the LAN / tailnet. PostgreSQL stays local.
FERRETDB_LISTEN_ADDR="${FERRETDB_LISTEN_ADDR:-0.0.0.0:27017}"
# Application credentials. The same user/password is both the PostgreSQL role
# FerretDB connects with AND the MongoDB user clients authenticate as.
FERRETDB_USER="${FERRETDB_USER:-ferretdb}"
FERRETDB_PASSWORD="${FERRETDB_PASSWORD:-}" # auto-generated when empty
# Optional: pre-authorise the LXC's Tailscale non-interactively.
# Generate at https://login.tailscale.com/admin/settings/keys
TS_AUTHKEY="${TS_AUTHKEY:-}"
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
# testing from a non-main branch.
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh}"
VERSION_FILE="${VERSION_FILE:-/opt/ferretdb_version.txt}"
# --- 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; }
# ============================================================
# Generic helpers
# ============================================================
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
}
# Debian packages use dpkg-style arch names (amd64, arm64), which is also what
# both the FerretDB and DocumentDB release assets are named with.
get_arch() {
case "$(uname -m)" in
x86_64) echo "amd64" ;;
aarch64) echo "arm64" ;;
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
}
# Resolve the DocumentDB release tag into the two coupled versions it encodes.
# Sets globals: DOC_TAG (full tag), DOC_PKG_VER (documentdb pkg version),
# FERRET_VER (matching FerretDB version, no leading v).
# Tag shape: v0.107.0-ferretdb-2.7.0
resolve_versions() {
local endpoint tag
if [[ "$DOCUMENTDB_TAG" == "latest" ]]; then
endpoint="${DOCUMENTDB_RELEASES_URL}/latest"
else
endpoint="${DOCUMENTDB_RELEASES_URL}/tags/${DOCUMENTDB_TAG}"
fi
tag=$(curl -fsSL "$endpoint" | jq -r '.tag_name')
if [[ -z "$tag" || "$tag" == "null" ]]; then
log_error "Failed to resolve DocumentDB release '${DOCUMENTDB_TAG}' from GitHub API."
exit 1
fi
DOC_TAG="$tag"
DOC_PKG_VER="${tag#v}" # 0.107.0-ferretdb-2.7.0
DOC_PKG_VER="${DOC_PKG_VER%%-ferretdb-*}" # 0.107.0
FERRET_VER="${tag##*-ferretdb-}" # 2.7.0
if [[ -z "$DOC_PKG_VER" || -z "$FERRET_VER" || "$FERRET_VER" == "$tag" ]]; then
log_error "Could not parse DocumentDB tag '${tag}' (expected vX-ferretdb-Y)."
exit 1
fi
}
# ============================================================
# Reusable: install or upgrade PostgreSQL + DocumentDB + FerretDB.
# Used by both fresh-install and update flows. Idempotent.
# ============================================================
install_or_upgrade_packages() {
local arch current doc_url ferret_url tmpdir doc_deb_name doc_full_ver
resolve_versions
arch=$(get_arch)
current=""
if [[ -f "$VERSION_FILE" ]]; then
current=$(cat "$VERSION_FILE")
fi
if [[ "$current" == "$DOC_TAG" && -x /usr/bin/ferretdb ]]; then
log_info "FerretDB stack already at ${DOC_TAG}, nothing to do."
return 0
fi
# Ensure the PGDG repo is present so the requested PostgreSQL major exists.
if [[ ! -f /etc/apt/sources.list.d/pgdg.list ]]; then
log_info "Adding the PostgreSQL APT (PGDG) repository..."
install -d -m 0755 /usr/share/keyrings
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| gpg --dearmor -o /usr/share/keyrings/postgresql.gpg
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo "$VERSION_CODENAME")-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
apt-get update >/dev/null
fi
log_info "Installing PostgreSQL ${PG_VERSION} + pg_cron..."
DEBIAN_FRONTEND=noninteractive apt-get install -y \
"postgresql-${PG_VERSION}" "postgresql-${PG_VERSION}-cron" >/dev/null
# documentdb deb naming: deb12-postgresql-17-documentdb_0.107.0.ferretdb.2.7.0_amd64.deb
doc_full_ver="${DOC_PKG_VER}.ferretdb.${FERRET_VER}"
doc_deb_name="${DOCUMENTDB_DISTRO}-postgresql-${PG_VERSION}-documentdb_${doc_full_ver}_${arch}.deb"
doc_url="https://github.com/FerretDB/documentdb/releases/download/${DOC_TAG}/${doc_deb_name}"
ferret_url="https://github.com/FerretDB/FerretDB/releases/download/v${FERRET_VER}/ferretdb-${arch}-linux.deb"
tmpdir=$(mktemp -d)
log_info "Downloading DocumentDB extension (${doc_deb_name})..."
curl -fsSL "$doc_url" -o "${tmpdir}/documentdb.deb"
log_info "Downloading FerretDB ${FERRET_VER} (${arch})..."
curl -fsSL "$ferret_url" -o "${tmpdir}/ferretdb.deb"
log_info "Installing DocumentDB extension + FerretDB (apt resolves dependencies)..."
DEBIAN_FRONTEND=noninteractive apt-get install -y \
"${tmpdir}/documentdb.deb" "${tmpdir}/ferretdb.deb" >/dev/null
# If the extension is already created (update path), bring it to the new version.
if su -s /bin/sh postgres -c "psql -tAc \"SELECT 1 FROM pg_extension WHERE extname='documentdb'\" -d postgres" 2>/dev/null | grep -q 1; then
log_info "Updating documentdb extension to ${DOC_PKG_VER}..."
su -s /bin/sh postgres -c "psql -d postgres -c 'ALTER EXTENSION documentdb UPDATE;'" >/dev/null 2>&1 || \
log_warn "ALTER EXTENSION documentdb UPDATE failed — check after restart."
fi
echo "$DOC_TAG" > "$VERSION_FILE"
rm -rf "$tmpdir"
log_info "Installed FerretDB: $(/usr/bin/ferretdb --version 2>&1 | head -n1 || true)"
# Restart services if they already exist (update path); the install path
# enables them explicitly after configuration.
if systemctl list-unit-files ferretdb.service >/dev/null 2>&1; then
systemctl restart postgresql 2>/dev/null || true
systemctl restart ferretdb 2>/dev/null || true
fi
}
# ============================================================
# Reusable: bring Tailscale up so the LXC joins the tailnet.
# Unlike openbao we do NOT use 'tailscale serve' — FerretDB speaks the raw
# MongoDB wire protocol (TCP), not HTTP, so serve does not apply. The DB is
# reachable directly on 0.0.0.0:27017 over the LAN / tailnet.
# ============================================================
configure_tailscale() {
if ! command -v tailscale >/dev/null 2>&1; then
log_warn "tailscale CLI not found, skipping tailnet setup."
return 0
fi
local backend_state
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
if [[ "$backend_state" == "Running" ]]; then
log_info "Tailscale already up."
return 0
fi
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 --hostname ${HOSTNAME_LXC}"
fi
}
# ============================================================
# Proxmox-host helpers
# ============================================================
# The installer re-fetches itself inside the LXC from SCRIPT_URL. Verify it is
# reachable on the host *before* creating any container, so a wrong branch/path
# fails immediately with guidance instead of dying mid-install with a curl 404.
preflight_script_url() {
if curl -fsSL -o /dev/null "$SCRIPT_URL"; then
return 0
fi
log_error "SCRIPT_URL is not reachable: ${SCRIPT_URL}"
log_error "The installer re-fetches itself inside the LXC from SCRIPT_URL, so this"
log_error "must resolve. If you are testing from a branch (not yet on main), pass it:"
log_error " SCRIPT_URL=https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/<branch>/ferretdb/install.sh \\"
log_error " bash -c \"\$(curl -fsSL \"\$SCRIPT_URL\")\""
exit 1
}
# Detect newest Debian *12* LXC template available from the Proxmox repos.
# Deliberately pinned to debian-12: the DocumentDB extension only ships a deb12
# build (DOCUMENTDB_DISTRO default), and a newer template (e.g. debian-13) would
# pair that deb against a different libicu soname. To move to trixie, set both
# TEMPLATE and DOCUMENTDB_DISTRO yourself once upstream publishes a deb13 build.
detect_latest_debian_template() {
local tmpl
tmpl=$(pveam available --section system 2>/dev/null \
| awk '/^system[[:space:]]+debian-12-/ {print $2}' \
| sort -V \
| tail -n1)
if [[ -z "$tmpl" ]]; then
log_warn "Could not find a debian-12 template via pveam; falling back to a known-good name."
tmpl="debian-12-standard_12.7-1_amd64.tar.zst"
fi
log_info "Selected Debian template: $tmpl"
echo "$tmpl"
}
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
find_existing_lxc() {
local id host tags
while read -r id _; do
[[ -z "$id" || "$id" == "VMID" ]] && continue
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
echo "$id"
return 0
fi
done < <(pct list | awk 'NR>1 {print $1}')
return 1
}
ensure_template_present() {
local tmpl="$1"
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
pveam update >/dev/null
pveam download "$TEMPLATE_STORAGE" "$tmpl"
else
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
fi
}
# Pick next available CTID if user did not provide one.
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 relevant runtime configuration through the environment so the
# inner invocation produces the same config the user requested on the host.
exec_in_lxc() {
local ctid="$1"
local mode="$2" # --install or --update
# Ensure base tooling exists inside the container before piping the script.
pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null 2>&1; apt-get install -y bash curl jq ca-certificates >/dev/null 2>&1"
curl -fsSL "$SCRIPT_URL" \
| pct exec "$ctid" -- env \
LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
SCRIPT_URL="$SCRIPT_URL" \
PG_VERSION="$PG_VERSION" \
DOCUMENTDB_TAG="$DOCUMENTDB_TAG" \
DOCUMENTDB_DISTRO="$DOCUMENTDB_DISTRO" \
FERRETDB_HOSTNAME="$HOSTNAME_LXC" \
FERRETDB_LISTEN_ADDR="$FERRETDB_LISTEN_ADDR" \
FERRETDB_USER="$FERRETDB_USER" \
FERRETDB_PASSWORD="$FERRETDB_PASSWORD" \
TS_AUTHKEY="$TS_AUTHKEY" \
bash -s -- "$mode"
}
# ============================================================
# MODE: Proxmox host — create LXC + install
# ============================================================
create_lxc() {
log_info "=== FerretDB — LXC creation ==="
# Generate the password on the host so we can both pass it in and print it.
if [[ -z "$FERRETDB_PASSWORD" ]]; then
FERRETDB_PASSWORD=$(openssl rand -hex 24)
log_info "Generated FerretDB password (saved in the summary below)."
fi
if [[ -z "$TEMPLATE" ]]; then
TEMPLATE=$(detect_latest_debian_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"
# Wait for network to come up
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 > 30 )); then
log_error "LXC ${CTID} did not acquire an IP after 30s."
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 " FerretDB : ${FERRETDB_LISTEN_ADDR}"
echo ""
echo "MongoDB connection string (point your client/app at it):"
echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-<ip>}:27017/"
echo ""
echo "Store this password somewhere safe — it is not persisted on the host:"
echo " user : ${FERRETDB_USER}"
echo " password : ${FERRETDB_PASSWORD}"
echo ""
}
# ============================================================
# MODE: Proxmox host — update existing LXC
# ============================================================
update_lxc() {
local ctid="$1"
log_info "=== FerretDB — 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 "Refreshing Debian packages inside LXC ${ctid}..."
pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null && apt-get upgrade -y >/dev/null"
log_info "Upgrading FerretDB stack inside LXC ${ctid}..."
exec_in_lxc "$ctid" "--update"
log_info "Update of LXC ${ctid} complete."
}
# ============================================================
# MODE: inside LXC — fresh install of FerretDB
# ============================================================
install_inside_lxc() {
log_info "=== FerretDB — installation ==="
if [[ -z "$FERRETDB_PASSWORD" ]]; then
FERRETDB_PASSWORD=$(openssl rand -hex 24 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
log_info "Generated FerretDB password (shown in the summary below)."
fi
log_info "Updating package index..."
export DEBIAN_FRONTEND=noninteractive
apt-get update >/dev/null
apt-get upgrade -y >/dev/null
log_info "Installing base dependencies..."
apt-get install -y curl jq ca-certificates gnupg lsb-release sudo logrotate openssl >/dev/null
log_info "Installing Tailscale..."
if ! command -v tailscale >/dev/null 2>&1; then
curl -fsSL https://tailscale.com/install.sh | sh >/dev/null 2>&1 \
|| log_warn "Tailscale install script failed — install it manually later."
fi
systemctl enable --now tailscaled >/dev/null 2>&1 \
|| log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)"
install_or_upgrade_packages
log_info "Configuring PostgreSQL for DocumentDB..."
local pg_confd="/etc/postgresql/${PG_VERSION}/main/conf.d"
mkdir -p "$pg_confd"
# https://docs.ferretdb.io/installation/documentdb/deb/
cat > "${pg_confd}/documentdb.conf" <<EOF
# Managed by infra-scripts/ferretdb — DocumentDB extension settings.
shared_preload_libraries = 'pg_cron,pg_documentdb_core,pg_documentdb'
cron.database_name = 'postgres'
documentdb.enableCompact = true
documentdb.enableLetAndCollationForQueryMatch = true
documentdb.enableNowSystemVariable = true
documentdb.enableSortbyIdPushDownToPrimaryKey = true
documentdb.enableSchemaValidation = true
documentdb.enableBypassDocumentValidation = true
documentdb.enableUserCrud = true
documentdb.maxUserLimit = 100
# Ensure any password we set is hashed with SCRAM-SHA-256 (PG default, set
# explicitly so it is active before roles are provisioned).
password_encryption = 'scram-sha-256'
# Postgres stays loopback-only; FerretDB (same LXC) is the network front door.
listen_addresses = '127.0.0.1'
EOF
log_info "Restarting PostgreSQL..."
systemctl restart postgresql
log_info "Creating the documentdb extension..."
su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null <<'SQL'
CREATE EXTENSION IF NOT EXISTS documentdb CASCADE;
SQL
# Provision the user THROUGH DocumentDB — never a plain CREATE ROLE ... PASSWORD.
# DocumentDB builds the SCRAM-SHA-256 verifier with its own salt length
# (documentdb.scramDefaultSaltLen = 28 bytes) via documentdb_api.create_user /
# update_user. A native PostgreSQL role instead stores a 16-byte salt, which
# MongoDB clients reject at SASL step2 with "invalid salt length of 16".
# create_user only accepts a read-only role, or the clusterAdmin +
# readWriteAnyDatabase pair we use here for full read/write (FerretDB/DocumentDB
# commands/users.c::ValidateAndObtainUserRole). The spec is built with jq so the
# password is JSON-escaped, then embedded in a $DDB$-dollar-quoted SQL literal.
log_info "Provisioning MongoDB user '${FERRETDB_USER}' via DocumentDB (28-byte SCRAM salt)..."
local role_exists cmd spec
role_exists=$(su -s /bin/sh postgres -c \
"psql -tAX -d postgres -c \"SELECT 1 FROM pg_roles WHERE rolname = '${FERRETDB_USER}'\"" 2>/dev/null || true)
if [[ "$role_exists" == "1" ]]; then
log_info "Role '${FERRETDB_USER}' already exists — resetting its password via DocumentDB."
cmd="update_user"
spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \
'{updateUser:$u, pwd:$p}')
else
cmd="create_user"
spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \
'{createUser:$u, pwd:$p, roles:[{role:"clusterAdmin",db:"admin"},{role:"readWriteAnyDatabase",db:"admin"}]}')
fi
su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null <<SQL
SELECT documentdb_api.${cmd}(\$DDB\$${spec}\$DDB\$);
SQL
log_info "Writing FerretDB systemd override..."
mkdir -p /etc/systemd/system/ferretdb.service.d
cat > /etc/systemd/system/ferretdb.service.d/override.conf <<EOF
[Service]
Environment=FERRETDB_POSTGRESQL_URL=postgres://${FERRETDB_USER}:${FERRETDB_PASSWORD}@127.0.0.1:5432/postgres
Environment=FERRETDB_LISTEN_ADDR=${FERRETDB_LISTEN_ADDR}
Environment=FERRETDB_TELEMETRY=disable
EOF
chmod 600 /etc/systemd/system/ferretdb.service.d/override.conf
systemctl daemon-reload
log_info "Enabling and starting services..."
systemctl enable --now postgresql >/dev/null 2>&1 || true
systemctl enable ferretdb >/dev/null 2>&1 || true
# Explicit restart: the deb postinst may have already started ferretdb with
# default settings, in which case 'enable --now' would not re-read our override.
systemctl restart ferretdb || log_warn "ferretdb failed to start — check 'journalctl -u ferretdb'."
# --- Log hygiene ---
# PostgreSQL ships /etc/logrotate.d/postgresql-common already. FerretDB logs to
# journald, so bound the journal instead of adding a logrotate stanza.
log_info "Bounding the systemd journal size..."
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/ferretdb.conf <<'EOF'
[Journal]
SystemMaxUse=200M
EOF
systemctl restart systemd-journald >/dev/null 2>&1 || true
# --- Console auto-login on tty1 (Proxmox web console / pct console) ---
log_info "Enabling console auto-login on tty1..."
mkdir -p /etc/systemd/system/container-getty@1.service.d
cat > /etc/systemd/system/container-getty@1.service.d/autologin.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM
EOF
systemctl daemon-reload
systemctl restart container-getty@1.service 2>/dev/null || true
configure_tailscale
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 except the values we want
# frozen at install time, which we inject via a small companion env file.
cat > /etc/ferretdb-motd.env <<EOF
FERRETDB_USER='${FERRETDB_USER}'
FERRETDB_LISTEN_ADDR='${FERRETDB_LISTEN_ADDR}'
EOF
chmod 600 /etc/ferretdb-motd.env
cat > /etc/profile.d/00-ferretdb.sh <<'MOTD'
[ -f /etc/ferretdb-motd.env ] && . /etc/ferretdb-motd.env
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"
LAN_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
FERRET_VERSION=$(/usr/bin/ferretdb --version 2>/dev/null | head -n1 || echo "unknown")
systemctl is-active --quiet ferretdb && FERRET_STATE="active" || FERRET_STATE="DOWN"
systemctl is-active --quiet postgresql && PG_STATE="active" || PG_STATE="DOWN"
echo ""
echo " _____ _ ____ ____ "
echo "| ___|__ _ __ _ __ ___| |_| _ \\| __ ) "
echo "| |_ / _ \\ '__| '__/ _ \\ __| | | | _ \\ "
echo "| _| __/ | | | | __/ |_| |_| | |_) |"
echo "|_| \\___|_| |_| \\___|\\__|____/|____/ "
echo ""
echo "FerretDB (MongoDB-compatible) — ${FERRET_VERSION}"
echo "─────────────────────────────────────────"
echo "Status:"
echo " • FerretDB : ${FERRET_STATE} (listening on ${FERRETDB_LISTEN_ADDR})"
echo " • PostgreSQL : ${PG_STATE} (127.0.0.1:5432)"
echo ""
echo "Connection string (MongoDB URI):"
echo " mongodb://${FERRETDB_USER}:<password>@${LAN_IP:-<ip>}:27017/"
echo " (tailnet) mongodb://${FERRETDB_USER}:<password>@${TS_FQDN}:27017/"
echo ""
echo "Useful commands:"
echo " systemctl status ferretdb postgresql"
echo " journalctl -u ferretdb -f"
echo " mongosh \"mongodb://${FERRETDB_USER}:<password>@127.0.0.1:27017/\""
echo "─────────────────────────────────────────"
echo ""
MOTD
chmod +x /etc/profile.d/00-ferretdb.sh
log_info "Cleaning up..."
apt-get clean >/dev/null 2>&1 || true
local ip
ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
echo ""
log_info "========================================="
log_info "FerretDB installation complete!"
log_info "========================================="
echo ""
echo "MongoDB connection string (point your client/app at it):"
echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-<ip>}:27017/"
echo ""
echo "Credentials (store safely — not persisted on the Proxmox host):"
echo " user : ${FERRETDB_USER}"
echo " password : ${FERRETDB_PASSWORD}"
echo ""
}
# ============================================================
# MODE: inside LXC — update only
# ============================================================
update_inside_lxc() {
log_info "=== FerretDB — update ==="
export DEBIAN_FRONTEND=noninteractive
apt-get update >/dev/null
apt-get upgrade -y >/dev/null
install_or_upgrade_packages
configure_tailscale
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
preflight_script_url
local existing=""
if existing=$(find_existing_lxc); then
log_info "Found existing FerretDB 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 [[ -x /usr/bin/ferretdb ]]; then
update_inside_lxc
else
install_inside_lxc
fi
fi
}
main "$@"
+3 -2
View File
@@ -20,7 +20,7 @@ Single script, three automatic modes:
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)" bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"
``` ```
The script automatically creates an Alpine 3.23 LXC with Docker and act_runner. The script automatically creates an Alpine LXC (template auto-detected from `pveam available`) with Docker and act_runner.
#### Customization #### Customization
@@ -34,6 +34,7 @@ CTID=120 HOSTNAME=runner-02 CORES=4 RAM=4096 bash -c "$(curl -fsSL https://gitea
|----------|---------|-------------| |----------|---------|-------------|
| `CTID` | auto | Container ID | | `CTID` | auto | Container ID |
| `RUNNER_HOSTNAME` | `gitea-runner` | LXC Hostname | | `RUNNER_HOSTNAME` | `gitea-runner` | LXC Hostname |
| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` |
| `CORES` | `2` | CPU cores | | `CORES` | `2` | CPU cores |
| `RAM` | `2048` | RAM in MiB | | `RAM` | `2048` | RAM in MiB |
| `DISK` | `8` | Disk in GB | | `DISK` | `8` | Disk in GB |
@@ -59,7 +60,7 @@ From inside the LXC:
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash
``` ```
The script detects that act_runner is already installed and switches to update mode automatically. The script detects that act_runner is already installed and switches to update mode automatically. Re-running from the Proxmox host does the same, plus refreshes the LXC's Alpine packages first (`apk update && apk upgrade`).
### Architecture ### Architecture
+124 -39
View File
@@ -8,15 +8,19 @@ set -euo pipefail
# --- Config (override via environment) --- # --- Config (override via environment) ---
CTID="${CTID:-}" CTID="${CTID:-}"
HOSTNAME="${RUNNER_HOSTNAME:-gitea-runner}" HOSTNAME_LXC="${RUNNER_HOSTNAME:-gitea-runner}"
TEMPLATE="${TEMPLATE:-alpine-3.23-default_20260116_amd64.tar.xz}" TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
STORAGE="${STORAGE:-local-lvm}" STORAGE="${STORAGE:-local-lvm}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
CORES="${CORES:-2}" CORES="${CORES:-2}"
RAM="${RAM:-2048}" RAM="${RAM:-2048}"
DISK="${DISK:-8}" DISK="${DISK:-8}"
BRIDGE="${BRIDGE:-vmbr0}" BRIDGE="${BRIDGE:-vmbr0}"
SCRIPT_URL="https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh" LXC_TAG="${LXC_TAG:-gitea-runner}" # 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/chore/standardize-lxc-scripts/gitea-runner/install.sh"
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh}"
GITEA_HOSTNAME="${GITEA_HOSTNAME:-gitea.taila5ad8.ts.net}" GITEA_HOSTNAME="${GITEA_HOSTNAME:-gitea.taila5ad8.ts.net}"
GITEA_API="https://gitea.com/api/v1/repos/gitea/act_runner/releases" GITEA_API="https://gitea.com/api/v1/repos/gitea/act_runner/releases"
VERSION_FILE="/opt/gitea-runner_version.txt" VERSION_FILE="/opt/gitea-runner_version.txt"
@@ -27,9 +31,47 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } # Logs go to stderr so callers can safely use $(fn) without capturing log noise.
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
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
}
# ============================================================
# Load shared helpers (lib/common.sh: detect_latest_alpine_template,
# enable_tty1_autologin, find_existing_lxc, refresh_os_packages).
#
# Same reasoning as openbao/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 download the act_runner binary, 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
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 detect_latest_alpine_template
# et al. Fail loudly here instead, with the URL that was tried.
if ! declare -F detect_latest_alpine_template >/dev/null; then
log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})."
exit 1
fi
# --- Helpers --- # --- Helpers ---
get_latest_release() { get_latest_release() {
@@ -67,12 +109,34 @@ download_runner() {
echo "$release" > "$VERSION_FILE" echo "$release" > "$VERSION_FILE"
} }
# 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/install.sh's helper
# of the same name).
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="$GITEA_HOSTNAME" \
bash -s -- "$mode"
}
# ============================================================ # ============================================================
# MODE 1: Proxmox host — create LXC container # MODE 1: Proxmox host — create LXC container
# ============================================================ # ============================================================
create_lxc() { create_lxc() {
log_info "=== Gitea Act Runner — LXC Creation ===" log_info "=== Gitea Act Runner — LXC Creation ==="
if [[ -z "$TEMPLATE" ]]; then
TEMPLATE=$(detect_latest_alpine_template)
else
log_info "Using user-provided template: $TEMPLATE"
fi
# Auto-select next CTID if not specified # Auto-select next CTID if not specified
if [[ -z "$CTID" ]]; then if [[ -z "$CTID" ]]; then
CTID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \ CTID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
@@ -80,22 +144,18 @@ create_lxc() {
log_info "Auto-selected CTID: $CTID" log_info "Auto-selected CTID: $CTID"
fi fi
# Download template if needed ensure_template_present "$TEMPLATE"
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
log_info "Downloading template $TEMPLATE..."
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
fi
log_info "Creating LXC $CTID ($HOSTNAME)..." log_info "Creating LXC $CTID ($HOSTNAME_LXC)..."
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \ pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
--hostname "$HOSTNAME" \ --hostname "$HOSTNAME_LXC" \
--cores "$CORES" \ --cores "$CORES" \
--memory "$RAM" \ --memory "$RAM" \
--rootfs "${STORAGE}:${DISK}" \ --rootfs "${STORAGE}:${DISK}" \
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \ --net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
--unprivileged 1 \ --unprivileged 1 \
--features nesting=1,keyctl=1 \ --features nesting=1,keyctl=1 \
--tags "infra-script,cicd" \ --tags "infra-script,${LXC_TAG}" \
--start 0 --start 0
log_info "Configuring LXC for Docker and Tailscale..." log_info "Configuring LXC for Docker and Tailscale..."
@@ -111,8 +171,7 @@ EOF
sleep 5 sleep 5
log_info "Injecting install script into container..." log_info "Injecting install script into container..."
pct exec "$CTID" -- sh -c "apk add --no-cache bash curl jq > /dev/null 2>&1" exec_in_lxc "$CTID" "--install"
curl -fsSL "$SCRIPT_URL" | pct exec "$CTID" -- bash -s -- --install
local ip local ip
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1) ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
@@ -122,7 +181,7 @@ EOF
log_info "LXC $CTID created successfully!" log_info "LXC $CTID created successfully!"
log_info "=========================================" log_info "========================================="
echo "" echo ""
echo " Hostname : $HOSTNAME" echo " Hostname : $HOSTNAME_LXC"
echo " IP : ${ip:-pending}" echo " IP : ${ip:-pending}"
echo "" echo ""
echo "Next steps:" echo "Next steps:"
@@ -133,6 +192,28 @@ EOF
echo "" echo ""
} }
# ============================================================
# MODE 1b: Proxmox host — update an existing LXC
# ============================================================
update_lxc() {
local ctid="$1"
log_info "=== Gitea Act Runner — 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 "Refreshing Alpine packages inside LXC ${ctid}..."
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
log_info "Upgrading act_runner binary inside LXC ${ctid}..."
exec_in_lxc "$ctid" "--update"
log_info "Update of LXC ${ctid} complete."
}
# ============================================================ # ============================================================
# MODE 2: Inside LXC — fresh install # MODE 2: Inside LXC — fresh install
# ============================================================ # ============================================================
@@ -232,24 +313,7 @@ LOGROTATE
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
log_info "Enabling console auto-login on tty1..." enable_tty1_autologin
# Alpine ships busybox getty by default; agetty (from util-linux) is what
# supports --autologin.
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
# Replace any existing tty1 entry, then append our autologin line. Doing it
# in two steps (delete + append) is more robust than an in-place sed against
# a pattern that may drift across Alpine releases.
sed -i '/^tty1::/d' /etc/inittab
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
kill -HUP 1 2>/dev/null || true
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
# the new line — otherwise the first web-console session lands on the stale
# process and the operator has to type `exit` once before autologin kicks in.
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
log_info "Cleaning up..." log_info "Cleaning up..."
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
@@ -275,6 +339,8 @@ LOGROTATE
update_runner() { update_runner() {
log_info "=== Gitea Act Runner — Update ===" log_info "=== Gitea Act Runner — Update ==="
refresh_os_packages
local release local release
release=$(get_latest_release) release=$(get_latest_release)
@@ -305,19 +371,38 @@ update_runner() {
# Main — detect context # Main — detect context
# ============================================================ # ============================================================
main() { main() {
if [[ "${1:-}" == "--install" ]]; then case "${1:-}" in
# Explicitly called in install mode (from pct exec) --install)
install_runner install_runner
elif command -v pct &> /dev/null; then return
;;
--update)
update_runner
return
;;
esac
if command -v pct &> /dev/null; then
# We're on the Proxmox host # We're on the Proxmox host
require_root
local existing=""
if existing=$(find_existing_lxc); then
log_info "Found existing gitea-runner LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
update_lxc "$existing"
else
create_lxc create_lxc
elif [[ -f /usr/local/bin/act_runner ]]; then fi
else
# Inside a container (no Proxmox tooling)
require_root
if [[ -f /usr/local/bin/act_runner ]]; then
# act_runner exists — update mode # act_runner exists — update mode
update_runner update_runner
else else
# Fresh LXC — install mode # Fresh LXC — install mode
install_runner install_runner
fi fi
fi
} }
main "$@" main "$@"
+120
View File
@@ -0,0 +1,120 @@
# lib/common.sh - Shared helpers for Proxmox LXC creator scripts.
#
# Sourced (not executed) by openbao/install.sh and gitea-runner/install.sh.
# Assumes the sourcing script already defines log_info/log_warn/log_error
# (both scripts do, identically) — this file does not redefine them.
#
# Contract for future LXC creator scripts:
# - detect_latest_alpine_template(): Alpine only. A future Debian-based
# script needs its own detect_latest_debian_template() (same pattern:
# pveam available + sort -V + hardcoded fallback) — do not overload
# this function with an OS parameter.
# - enable_tty1_autologin(): implements the Alpine/OpenRC autologin
# mechanism (inittab + agetty). A future Debian-based script needs a
# distinct function (systemd container-getty override) rather than a
# branch inside this one.
# - find_existing_lxc(): OS-agnostic, works by tag/hostname via `pct
# config`. Reusable as-is by any LXC creator script.
# - refresh_os_packages(): Alpine only (apk update && apk upgrade). A
# future Debian-based script needs its own apt-get variant.
#
# Does not set shell options (set -e/-u/-o pipefail): a sourced file must
# not impose those on the caller's shell. Both openbao/install.sh and
# gitea-runner/install.sh already set them before sourcing this file.
# ============================================================
# #12 - Detect newest Alpine LXC template available from the Proxmox repos.
# Echoes the template filename. Falls back to a hardcoded known-good
# template if `pveam` is unavailable or returns nothing.
# ============================================================
detect_latest_alpine_template() {
local tmpl
tmpl=$(pveam available --section system 2>/dev/null \
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
| sort -V \
| tail -n1)
if [[ -z "$tmpl" ]]; then
log_warn "Could not query pveam; falling back to a known-good Alpine template."
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
fi
log_info "Selected Alpine template: $tmpl"
echo "$tmpl"
}
# ============================================================
# #14 - Enable root auto-login on tty1 for an Alpine/OpenRC LXC.
# Idempotent: safe to call on every install/update.
# ============================================================
enable_tty1_autologin() {
log_info "Enabling console auto-login on tty1..."
# Alpine ships busybox getty by default; agetty (from util-linux) is what
# supports --autologin.
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
# Replace any existing tty1 entry, then append our autologin line. Doing it
# in two steps (delete + append) is more robust than an in-place sed against
# a pattern that may drift across Alpine releases.
sed -i '/^tty1::/d' /etc/inittab
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
kill -HUP 1 2>/dev/null || true
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
# the new line — otherwise the first web-console session lands on the stale
# process and the operator has to type `exit` once before autologin kicks in.
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
}
# ============================================================
# #12 - Ensure the given template is downloaded to TEMPLATE_STORAGE, doing a
# `pveam update` first so a stale local cache doesn't silently settle for an
# older version than the one detect_latest_alpine_template() just picked.
# Expects TEMPLATE_STORAGE to be set by the caller.
# ============================================================
ensure_template_present() {
local tmpl="$1"
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
pveam update >/dev/null
pveam download "$TEMPLATE_STORAGE" "$tmpl"
else
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
fi
}
# ============================================================
# #15 - Find an existing LXC by tag or hostname (host-side, requires pct).
# Echoes the CTID on match, returns 1 if none found.
#
# Expects HOSTNAME_LXC and LXC_TAG to be set by the caller (as openbao's
# find_existing_lxc already does).
# ============================================================
find_existing_lxc() {
local id host tags
while read -r id _; do
[[ -z "$id" || "$id" == "VMID" ]] && continue
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
echo "$id"
return 0
fi
done < <(pct list | awk 'NR>1 {print $1}')
return 1
}
# ============================================================
# #15 - Refresh OS packages (Alpine: apk update && apk upgrade).
# Callable only from inside the LXC: this is a plain bash function in the
# current process, so it cannot run across a `pct exec ... sh -c` boundary
# without shipping its definition into the container. Host-side callers
# (see openbao/install.sh's update_lxc()) invoke apk update/upgrade inline
# via `pct exec` instead — do not try to dedupe that call site onto this
# function.
# ============================================================
refresh_os_packages() {
log_info "Refreshing Alpine packages..."
apk update >/dev/null && apk upgrade >/dev/null
}
+3 -1
View File
@@ -110,7 +110,9 @@ curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-Open
The script auto-detects the presence of `/usr/local/bin/bao` and switches to The script auto-detects the presence of `/usr/local/bin/bao` and switches to
update mode. The OpenRC service is stopped, the binary is swapped (the old one update mode. The OpenRC service is stopped, the binary is swapped (the old one
is kept as `bao.bak.<ts>`), then the service is restarted. is kept as `bao.bak.<ts>`), then the service is restarted. Re-running from the
Proxmox host does the same, plus refreshes the LXC's Alpine packages first
(`apk update && apk upgrade`).
### Architecture ### Architecture
+38 -62
View File
@@ -56,6 +56,39 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
log_error() { echo -e "${RED}[ERROR]${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).
#
# This script runs in three different contexts, only one of which has a
# real file on disk next to it:
# - local checkout (`bash openbao/install.sh`) -> lib/common.sh
# sits right there at ../lib/common.sh, source it straight from disk.
# - Proxmox host, documented one-liner (`bash -c "$(curl ... )"`)
# -> no checkout, no BASH_SOURCE path worth trusting.
# - inside the LXC (exec_in_lxc does `curl ... | pct exec ... bash -s --`)
# -> same story, script arrives on stdin.
# For the latter two we fetch lib/common.sh over HTTP, next to SCRIPT_URL.
# The LXC already needs outbound network to curl this very script and to
# download the bao binary, 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
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 detect_latest_alpine_template
# et al. Fail loudly here instead, with the URL that was tried.
if ! declare -F detect_latest_alpine_template >/dev/null; then
log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})."
exit 1
fi
# ============================================================ # ============================================================
# Generic helpers # Generic helpers
# ============================================================ # ============================================================
@@ -204,48 +237,6 @@ configure_tailscale_proxy() {
# Proxmox-host helpers # Proxmox-host helpers
# ============================================================ # ============================================================
# Detect newest Alpine LXC template available from the Proxmox repos.
detect_latest_alpine_template() {
local tmpl
tmpl=$(pveam available --section system 2>/dev/null \
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
| sort -V \
| tail -n1)
if [[ -z "$tmpl" ]]; then
log_warn "Could not query pveam; falling back to a known-good Alpine template."
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
fi
log_info "Selected Alpine template: $tmpl"
echo "$tmpl"
}
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
find_existing_lxc() {
local id host tags
while read -r id _; do
[[ -z "$id" || "$id" == "VMID" ]] && continue
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
echo "$id"
return 0
fi
done < <(pct list | awk 'NR>1 {print $1}')
return 1
}
ensure_template_present() {
local tmpl="$1"
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
pveam update >/dev/null
pveam download "$TEMPLATE_STORAGE" "$tmpl"
else
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
fi
}
# Pick next available CTID if user did not provide one. # Pick next available CTID if user did not provide one.
allocate_ctid() { allocate_ctid() {
pvesh get /cluster/nextid 2>/dev/null \ pvesh get /cluster/nextid 2>/dev/null \
@@ -363,6 +354,9 @@ update_lxc() {
fi fi
log_info "Refreshing Alpine packages inside LXC ${ctid}..." log_info "Refreshing Alpine packages inside LXC ${ctid}..."
# refresh_os_packages() is a bash function local to this process; it can't
# run over `pct exec ... sh -c` without shipping the function definition
# into the container, so this call site stays inline rather than dedupe.
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null" pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
log_info "Upgrading bao binary inside LXC ${ctid}..." log_info "Upgrading bao binary inside LXC ${ctid}..."
@@ -474,24 +468,7 @@ EOF
log_info "Starting openbao service..." log_info "Starting openbao service..."
rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log" rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log"
log_info "Enabling console auto-login on tty1..." enable_tty1_autologin
# Alpine ships busybox getty by default; agetty (from util-linux) is what
# supports --autologin.
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
# Replace any existing tty1 entry, then append our autologin line. Doing it
# in two steps (delete + append) is more robust than an in-place sed against
# a pattern that may drift across Alpine releases.
sed -i '/^tty1::/d' /etc/inittab
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
kill -HUP 1 2>/dev/null || true
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
# the new line — otherwise the first web-console session lands on the stale
# process and the operator has to type `exit` once before autologin kicks in.
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
configure_tailscale_proxy configure_tailscale_proxy
@@ -563,8 +540,7 @@ MOTD
# ============================================================ # ============================================================
update_inside_lxc() { update_inside_lxc() {
log_info "=== OpenBao — update ===" log_info "=== OpenBao — update ==="
apk update >/dev/null refresh_os_packages
apk upgrade >/dev/null
install_or_upgrade_bao install_or_upgrade_bao
configure_tailscale_proxy configure_tailscale_proxy
log_info "Update complete." log_info "Update complete."