feat(deploy): unattended install from a single answers file

--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 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-26 00:40:33 +02:00
parent 062d337dd0
commit ac86efd9c3
4 changed files with 102 additions and 3 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ Thumbs.db
# Gitea push token (parent home dir; never track)
.env.gitea
clupilot.env

View File

@ -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

View File

@ -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

View File

@ -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