From ddd22b175e753c389b18d858614499ca85df6f5d Mon Sep 17 00:00:00 2001 From: "l.kirchner" Date: Fri, 5 Jun 2026 02:28:20 +0200 Subject: [PATCH 1/3] Add webapp installer: Next.js site via self-hosted Gitea Actions runner Host-side script that creates an unprivileged Debian 12 LXC, installs Node.js + act_runner in host mode, and registers it against the Gitea instance. Deploy logic lives in the repo's .gitea/workflows/deploy.yml (deploy-as-code); the runner polls outbound, so no inbound port. --- ct/webapp.sh | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 ct/webapp.sh diff --git a/ct/webapp.sh b/ct/webapp.sh new file mode 100644 index 0000000..eab10f6 --- /dev/null +++ b/ct/webapp.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# webapp — Next.js site deployed via a self-hosted Gitea Actions runner +# +# Creates an unprivileged Debian 12 LXC that: +# - installs Node.js + a Gitea act_runner in HOST mode (no Docker, no inbound port) +# - serves the built site with `next start` on :APP_PORT (behind your proxy) +# - lets the repo's .gitea/workflows/deploy.yml build & deploy on every push +# (deploy-as-code; the runner polls Gitea outbound, so nothing is exposed) +# +# Run on a Proxmox VE host: +# bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/webapp.sh)" + +set -euo pipefail + +APP="webapp" +APP_DESCRIPTION="Next.js site deployed via a self-hosted Gitea Actions runner (deploy-as-code)" +APP_PORT="${APP_PORT:-3000}" + +LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" +INSTALL_SCRIPT_URL="${INSTALL_SCRIPT_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/install/webapp-install.sh}" + +# LXC defaults (Next build is memory-hungry; runner workspace + live copy need disk) +DEFAULT_HOSTNAME="web" +DEFAULT_DISK="30" +DEFAULT_CORES="2" +DEFAULT_RAM="4096" + +# App / runner defaults (all overridable via env) +DEFAULT_GITEA_INSTANCE_URL="https://gitea.luki-net.org" +DEFAULT_REPO_URL="https://gitea.luki-net.org/l.kirchner/Redesign-ad2b.git" +DEFAULT_SITE_URL="https://sichere-wirtschaft.de" +DEFAULT_SANITY_DATASET="production" +DEFAULT_SANITY_API_VERSION="2026-06-02" +DEFAULT_NODE_MAJOR="22" +DEFAULT_RUNNER_VERSION="0.2.13" +DEFAULT_RUNNER_LABELS="webapp:host" + +source <(curl -fsSL "$LIB_URL/build.func") + +# ── app-specific prompts (host TTY; each skipped if the var is preset) ─────── +prompt_app_config() { + echo + echo "── App / runner configuration ───────────────────────────────" + if [[ -z "${GITEA_INSTANCE_URL:-}" ]]; then + read -rp "Gitea instance URL [$DEFAULT_GITEA_INSTANCE_URL]: " GITEA_INSTANCE_URL + GITEA_INSTANCE_URL="${GITEA_INSTANCE_URL:-$DEFAULT_GITEA_INSTANCE_URL}" + fi + # Repo → Settings → Actions → Runners → "Create new runner" gives this token. + if [[ -z "${RUNNER_TOKEN:-}" ]]; then + read -rsp "Gitea runner registration token: " RUNNER_TOKEN; echo + fi + [[ -n "${RUNNER_TOKEN:-}" ]] || { msg_err "RUNNER_TOKEN is required (Repo → Settings → Actions → Runners)"; exit 1; } + if [[ -z "${REPO_URL:-}" ]]; then + read -rp "Website repo URL (informational) [$DEFAULT_REPO_URL]: " REPO_URL + REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" + fi + + if [[ -z "${NEXT_PUBLIC_SITE_URL:-}" ]]; then + read -rp "Public site URL [$DEFAULT_SITE_URL]: " NEXT_PUBLIC_SITE_URL + NEXT_PUBLIC_SITE_URL="${NEXT_PUBLIC_SITE_URL:-$DEFAULT_SITE_URL}" + fi + if [[ -z "${NEXT_PUBLIC_SANITY_PROJECT_ID:-}" ]]; then + read -rp "Sanity project ID: " NEXT_PUBLIC_SANITY_PROJECT_ID + fi + if [[ -z "${NEXT_PUBLIC_SANITY_DATASET:-}" ]]; then + read -rp "Sanity dataset [$DEFAULT_SANITY_DATASET]: " NEXT_PUBLIC_SANITY_DATASET + NEXT_PUBLIC_SANITY_DATASET="${NEXT_PUBLIC_SANITY_DATASET:-$DEFAULT_SANITY_DATASET}" + fi + if [[ -z "${NEXT_PUBLIC_SANITY_API_VERSION:-}" ]]; then + read -rp "Sanity API version [$DEFAULT_SANITY_API_VERSION]: " NEXT_PUBLIC_SANITY_API_VERSION + NEXT_PUBLIC_SANITY_API_VERSION="${NEXT_PUBLIC_SANITY_API_VERSION:-$DEFAULT_SANITY_API_VERSION}" + fi + if [[ -z "${NEXT_PUBLIC_CALENDLY_URL+x}" ]]; then + read -rp "Calendly URL (optional, empty for none): " NEXT_PUBLIC_CALENDLY_URL + fi + + NODE_MAJOR="${NODE_MAJOR:-$DEFAULT_NODE_MAJOR}" + RUNNER_VERSION="${RUNNER_VERSION:-$DEFAULT_RUNNER_VERSION}" + RUNNER_LABELS="${RUNNER_LABELS:-$DEFAULT_RUNNER_LABELS}" + RUNNER_NAME="${RUNNER_NAME:-$CT_HOSTNAME}" + + echo " → instance: $GITEA_INSTANCE_URL" + echo " → runner: $RUNNER_NAME labels: $RUNNER_LABELS (act_runner $RUNNER_VERSION, host mode)" + echo " → node: $NODE_MAJOR app port: $APP_PORT" +} + +# ── push gathered config into the container for the installer to consume ───── +push_app_config() { + msg_info "Pushing deploy config into container..." + local tmpf; tmpf=$(mktemp) + cat >"$tmpf" </dev/null | tr -d '\r\n') + cat < Date: Fri, 5 Jun 2026 02:29:15 +0200 Subject: [PATCH 2/3] Add webapp in-container installer (Node + act_runner host mode) Installs Node.js and act_runner in host mode, registers the runner as the unprivileged webapp user, and wires up systemd units for the runner and the next-start service. A narrow sudoers rule lets the runner restart only webapp.service; the build/deploy itself is driven by the repo workflow. --- install/webapp-install.sh | 195 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 install/webapp-install.sh diff --git a/install/webapp-install.sh b/install/webapp-install.sh new file mode 100644 index 0000000..9f243d4 --- /dev/null +++ b/install/webapp-install.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# webapp installer — runs inside the LXC, called by ct/webapp.sh +# +# Sets up a Next.js site deployed by a self-hosted Gitea Actions runner: +# - Node.js (NodeSource) + git + rsync + sudo +# - act_runner in HOST mode (no Docker), registered to the Gitea instance, +# running as the unprivileged `webapp` user and polling Gitea outbound +# - a systemd service running `next start` from /opt/webapp/current +# - a narrow sudoers rule so the runner may only restart that one service +# +# The actual build/deploy logic lives in the repo's .gitea/workflows/deploy.yml +# (deploy-as-code). The runner checks out, builds, syncs the result into +# /opt/webapp/current and restarts webapp.service. + +set -euo pipefail + +APP="webapp" +LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}" +source <(curl -fsSL "$LIB_URL/install.func") + +[[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; } + +# ── load deploy config pushed in by the host script ────────────────────────── +CONF="/root/webapp.deploy.env" +[[ -f "$CONF" ]] || { msg_err "$CONF not found (host bootstrap incomplete)"; exit 1; } +set -a; . "$CONF"; set +a + +: "${GITEA_INSTANCE_URL:?missing GITEA_INSTANCE_URL}" +: "${RUNNER_TOKEN:?missing RUNNER_TOKEN}" +APP_PORT="${APP_PORT:-3000}" +NODE_MAJOR="${NODE_MAJOR:-22}" +RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}" +RUNNER_LABELS="${RUNNER_LABELS:-webapp:host}" +RUNNER_NAME="${RUNNER_NAME:-$(hostname)}" + +APP_USER="webapp" +APP_HOME="/opt/webapp" +RUNNER_DIR="$APP_HOME/runner" +CURRENT_DIR="$APP_HOME/current" +CONF_DIR="/etc/webapp" +ENV_FILE="$CONF_DIR/env" + +run_user() { runuser -u "$APP_USER" -- env HOME="$APP_HOME" "$@"; } + +# ── packages: git + rsync + sudo ; Node.js via NodeSource ──────────────────── +setup_base_apt git rsync sudo + +NODE_HAVE="$(command -v node >/dev/null 2>&1 && node -v | sed -E 's/^v([0-9]+).*/\1/' || echo 0)" +if [[ "$NODE_HAVE" != "$NODE_MAJOR" ]]; then + msg_info "Installing Node.js ${NODE_MAJOR}.x (NodeSource)..." + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null + apt-get install -y -qq nodejs >/dev/null + msg_ok "Node $(node -v) installed" +else + msg_warn "Node $(node -v) already present, skipping" +fi + +# ── act_runner binary ───────────────────────────────────────────────────────── +if [[ ! -x /usr/local/bin/act_runner ]]; then + ARCH="$(dpkg --print-architecture)" + case "$ARCH" in amd64|arm64) ;; *) msg_err "unsupported arch: $ARCH"; exit 1 ;; esac + msg_info "Downloading act_runner $RUNNER_VERSION ($ARCH)..." + curl -fsSL "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${ARCH}" \ + -o /usr/local/bin/act_runner + chmod +x /usr/local/bin/act_runner + msg_ok "act_runner $(/usr/local/bin/act_runner --version 2>/dev/null | head -n1)" +else + msg_warn "act_runner already present, skipping download" +fi + +# ── user + dirs ────────────────────────────────────────────────────────────── +create_system_user "$APP_USER" "$APP_HOME" +mkdir -p "$RUNNER_DIR" "$CURRENT_DIR" "$CONF_DIR" +chown -R "$APP_USER:$APP_USER" "$APP_HOME" + +# ── env file: NEXT_PUBLIC_* are inlined at build time AND read at runtime ──── +# (kept on the host so the repo carries no environment-specific config; the +# workflow sources this file before `next build`.) +cat >"$ENV_FILE" </etc/sudoers.d/webapp-deploy </dev/null + +# ── systemd units ───────────────────────────────────────────────────────────── +# webapp.service: serves the built site from the live copy. Enabled (so it +# starts on boot) but not started now — the first workflow run populates +# /opt/webapp/current and starts it via the sudoers-allowed restart. +cat >/etc/systemd/system/webapp.service </etc/systemd/system/webapp-runner.service </dev/null 2>&1 +systemctl enable --now webapp-runner.service +msg_ok "systemd units installed (runner started; webapp.service enabled, starts on first deploy)" + +# ── notes / summary file ────────────────────────────────────────────────────── +CRED_FILE="/root/webapp.credentials" +cat >"$CRED_FILE" <} +Gitea instance: $GITEA_INSTANCE_URL +Runner name / labels: $RUNNER_NAME [$RUNNER_LABELS] (act_runner $RUNNER_VERSION, host mode) +Site (local): http://127.0.0.1:$APP_PORT (live after first deploy) +Live copy: $CURRENT_DIR +Site env (build+run): $ENV_FILE + +Deploy is driven by the repo workflow .gitea/workflows/deploy.yml +First deploy: push to the repo, OR repo -> Actions -> deploy -> "Run workflow" + +Manual restart: sudo systemctl restart webapp.service +Logs: journalctl -u webapp -u webapp-runner -f + +Prerequisites on the Gitea side: + - Actions enabled on the instance and on the repo (repo -> Settings -> Actions) + - The LXC needs outbound HTTPS to $GITEA_INSTANCE_URL and to github.com + (the latter only to fetch actions/checkout, unless you self-host actions) +EOF +chmod 600 "$CRED_FILE" + +# registration token already consumed → drop the bootstrap env file +shred -u "$CONF" 2>/dev/null || rm -f "$CONF" + +apt_cleanup +msg_ok "$APP installation finished" -- 2.54.0 From ab839b0e85ee876a7b7976d4798334945b2ad475 Mon Sep 17 00:00:00 2001 From: "l.kirchner" Date: Fri, 5 Jun 2026 02:29:35 +0200 Subject: [PATCH 3/3] README: add webapp to the available scripts table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5ba1c0b..f2553fc 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Inspired by [community-scripts/ProxmoxVE](https://github.com/community-scripts/P | App | Description | One-liner | |-----|-------------|-----------| | [devpi](ct/devpi.sh) | Private PyPI cache / mirror — saves time on CUDA/torch rebuilds | `bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/devpi.sh)"` | +| [webapp](ct/webapp.sh) | Next.js site with deploy-as-code via a self-hosted Gitea Actions runner (host mode, no inbound port) | `bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/webapp.sh)"` | ## Usage -- 2.54.0