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