From ac86efd9c3aacbd444c0964fa83b6a75860d451e Mon Sep 17 00:00:00 2001 From: nexxo Date: Sun, 26 Jul 2026 00:40:33 +0200 Subject: [PATCH] feat(deploy): unattended install from a single answers file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --env-file makes the whole run non-interactive, which is what a bare server needs: install git, clone, run, done. The file also carries the operational secrets (Hetzner DNS, Stripe, SMTP, the Proxmox key path); those are optional, so a first install can happen before the Stripe account exists and the features stay dark until the values are filled in. The installer warns when the file is readable by more than its owner, and clupilot.env is gitignored — it holds every secret an installation will need. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + deploy/clupilot.env.example | 39 ++++++++++++++++++++++++++++++++ deploy/install.sh | 44 +++++++++++++++++++++++++++++++++++++ docs/deployment.md | 21 +++++++++++++++--- 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 deploy/clupilot.env.example diff --git a/.gitignore b/.gitignore index 8fe29db..a6c3ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ Thumbs.db # Gitea push token (parent home dir; never track) .env.gitea +clupilot.env diff --git a/deploy/clupilot.env.example b/deploy/clupilot.env.example new file mode 100644 index 0000000..666bc39 --- /dev/null +++ b/deploy/clupilot.env.example @@ -0,0 +1,39 @@ +# CluPilot — answers for an unattended install. +# +# scp this to the new server as /root/clupilot.env +# chmod 600 /root/clupilot.env +# bash install.sh --env-file /root/clupilot.env +# +# Keep the master copy in your password manager, NOT in the repository. +# After a successful install the working values live in /opt/clupilot/.env, +# so this file can be shredded: shred -u /root/clupilot.env + +# ── Required ──────────────────────────────────────────────────────────────── +GIT_TOKEN= # Gitea token with read access to the repo +APP_DOMAIN=app.clupilot.com # customer portal +WWW_DOMAIN=www.clupilot.com # marketing site +WS_DOMAIN=ws.clupilot.com # websockets (Reverb) +ADMIN_DOMAIN=admin.clupilot.com # operator console — must NOT resolve publicly +ADMIN_EMAIL= +ADMIN_NAME=Administrator +ADMIN_PASSWORD= # at least 12 characters + +# ── Needed before the platform can actually do business ───────────────────── +# Without these the install still succeeds; the features stay dark. +HETZNER_DNS_TOKEN= # no token → no customer or host DNS names +CLUPILOT_DNS_ZONE=clupilot.com + +STRIPE_KEY= # pk_… publishable +STRIPE_SECRET= # sk_… secret +STRIPE_WEBHOOK_SECRET= # whsec_… per endpoint, differs test/live + +MAIL_MAILER=smtp # until this is set, mail only goes to the log +MAIL_HOST= +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM_ADDRESS=noreply@clupilot.com + +# Private key used to onboard Proxmox hosts. Put the key file on the server +# first (chmod 600) and point at it — a PEM does not fit in a .env line. +CLUPILOT_SSH_KEY_PATH=/opt/clupilot/secrets/proxmox_ed25519 diff --git a/deploy/install.sh b/deploy/install.sh index 3fdf083..4e52d6a 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -24,6 +24,20 @@ BRANCH="${BRANCH:-main}" # one in the docker group; the installer itself needs root only for apt. APP_USER="${APP_USER:-clupilot}" +# --env-file PATH — answers every prompt, so the whole run is unattended. +while [[ $# -gt 0 ]]; do + case "$1" in + --env-file) + ENV_FILE="${2:-}"; shift 2 ;; + --env-file=*) + ENV_FILE="${1#*=}"; shift ;; + -h|--help) + sed -n '2,20p' "$0"; exit 0 ;; + *) + echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; } die() { printf '\033[1;31m ✗\033[0m %s\n' "$*" >&2; exit 1; } @@ -47,6 +61,19 @@ ask_secret() { rand_b64() { head -c "${1:-32}" /dev/urandom | base64 -w0; } rand_hex() { head -c "${1:-24}" /dev/urandom | od -An -tx1 | tr -d ' \n'; } +if [[ -n "${ENV_FILE:-}" ]]; then + [[ -f "$ENV_FILE" ]] || { echo "No such file: $ENV_FILE" >&2; exit 1; } + + # It holds every secret this installation will ever need. A world-readable + # copy on a shared box is worth saying something about. + perms="$(stat -c '%a' "$ENV_FILE")" + [[ "$perms" == "600" || "$perms" == "400" ]] || \ + printf '\033[1;33m !\033[0m %s\n' "$ENV_FILE is mode $perms — chmod 600 it." + + # shellcheck disable=SC1090 # the path is the whole point of the flag + set -a; . "$ENV_FILE"; set +a +fi + # ── 0. Preflight ───────────────────────────────────────────────────────────── [[ $EUID -eq 0 ]] || die "Please run as root (sudo bash install.sh)." [[ -f /etc/os-release ]] || die "Unsupported system: no /etc/os-release." @@ -196,6 +223,23 @@ else # The container user is mapped to the service account, so files written # inside the container belong to it and not to root. + # Supplied through --env-file; skipped silently when absent, so a first + # install can happen before the Stripe account exists. + optional_env() { [[ -n "${2:-}" ]] && set_env "$1" "$2"; } + + optional_env HETZNER_DNS_TOKEN "${HETZNER_DNS_TOKEN:-}" + optional_env CLUPILOT_DNS_ZONE "${CLUPILOT_DNS_ZONE:-}" + optional_env STRIPE_KEY "${STRIPE_KEY:-}" + optional_env STRIPE_SECRET "${STRIPE_SECRET:-}" + optional_env STRIPE_WEBHOOK_SECRET "${STRIPE_WEBHOOK_SECRET:-}" + optional_env MAIL_MAILER "${MAIL_MAILER:-}" + optional_env MAIL_HOST "${MAIL_HOST:-}" + optional_env MAIL_PORT "${MAIL_PORT:-}" + optional_env MAIL_USERNAME "${MAIL_USERNAME:-}" + optional_env MAIL_PASSWORD "${MAIL_PASSWORD:-}" + optional_env MAIL_FROM_ADDRESS "${MAIL_FROM_ADDRESS:-}" + optional_env CLUPILOT_SSH_KEY_PATH "${CLUPILOT_SSH_KEY_PATH:-}" + set_env HOST_UID "$APP_UID" set_env HOST_GID "$APP_GID" set_env VITE_AUTOSTART false diff --git a/docs/deployment.md b/docs/deployment.md index 6776b82..008ea6a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -27,13 +27,28 @@ Owner account; close the firewall down to 22, 80, 443 and 51820/udp. It is safe to re-run: an existing `.env` is kept and the hub key is never regenerated — doing so would silently disconnect every onboarded host. -Every prompt has an environment variable, so it can also run unattended: +### Unattended, from a bare server + +The only thing to install by hand is git: ```bash -GIT_TOKEN=… ADMIN_EMAIL=… ADMIN_PASSWORD=… APP_DOMAIN=app.clupilot.com \ -ADMIN_DOMAIN=admin.clupilot.com bash deploy/install.sh +apt-get update && apt-get install -y git +git clone https://git.bave.dev/boban/CluPilotCloud.git /tmp/clupilot +bash /tmp/clupilot/deploy/install.sh --env-file /root/clupilot.env ``` +`deploy/clupilot.env.example` is the template. Put the real file on the server +as `/root/clupilot.env` with mode 600 **before** starting — the installer warns +if it is readable by anyone else. Keep the master copy in a password manager, +never in the repository (`clupilot.env` is gitignored for that reason). + +After a successful run the working values live in `/opt/clupilot/.env`, so the +bootstrap copy can go: `shred -u /root/clupilot.env`. + +The file also carries the operational secrets — Hetzner DNS, Stripe, SMTP, the +Proxmox SSH key path. Those are optional: the install succeeds without them and +the corresponding features simply stay dark until they are filled in. + ## Order of the update Migrations run **before** the new containers take traffic: code that expects a