2026-07-10 20:50:41 +00:00
#!/usr/bin/env bash
2026-07-11 08:25:48 +00:00
# rig coolify install — pinned Coolify install; AUTOUPDATE=false so the
2026-07-10 20:50:41 +00:00
# platform never self-updates underneath its operators. Upgrades are an
# explicit act.
set -euo pipefail
2026-07-18 14:50:46 +00:00
HERE = " $( cd " $( dirname " $( readlink -f " ${ BASH_SOURCE [0] } " ) " ) " && pwd ) "
# shellcheck source=SCRIPTDIR/lib/users-config.sh
. " $HERE /lib/users-config.sh " # read_role_marker — the traits line bootstrap wrote
log( ) { printf 'rig-coolify: %s\n' " $* " ; }
warn( ) { printf 'rig-coolify: WARNING: %s\n' " $* " >& 2; }
die( ) { printf 'rig-coolify: ERROR: %s\n' " $1 " >& 2; exit " ${ 2 :- 1 } " ; }
2026-07-10 20:50:41 +00:00
usage( ) {
cat <<'EOF'
2026-07-11 08:25:48 +00:00
usage: rig coolify install --version <pin>
2026-07-10 20:50:41 +00:00
Installs Coolify at exactly <pin> ( e.g. 4.1.2) with AUTOUPDATE = false.
Control-plane box only. The version pin is required — you state the floor
your tooling is verified against; there is no default.
EOF
}
VERSION = ""
while [ $# -gt 0 ] ; do
case " $1 " in
--version)
[ $# -ge 2 ] || die "--version needs a value" 2
VERSION = " $2 " ; shift 2 ; ;
-h| --help) usage; exit 0 ; ;
*) die " unknown flag: $1 " 2 ; ;
esac
done
if [ -z " $VERSION " ] ; then
usage >& 2
die "--version <pin> is required" 2
fi
2026-07-18 14:50:46 +00:00
# --- role-marker sanity (issue #25) ------------------------------------------
# Coolify belongs on the control-plane box and nowhere else — but the marker is
# ADVISORY, never a gate. It may legitimately be absent (a box bootstrapped
# before rig wrote markers, or a hand-built one), and rig refuses to guess from
# silence. When the marker EXISTS and names another role, the likeliest story
# is an operator in the wrong SSH session about to put a control plane on a
# workload box — so say it loudly. But WARN, never die: the operator may also
# be deliberately repurposing the box, and an advisory file must never outrank
# the human running the command (contrast close-root, where the marker IS the
# gate — shutting the root door blind is irreversible in a way an extra
# Coolify is not). Placed BEFORE the root check for the same reason arg errors
# are: the harness proves it non-root, and reading a 0644 file needs no
# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures
# (repo precedent: users-apply, users-close-root).
MARKER_LINE = " $( read_role_marker " ${ RIG_ROLE_MARKER :- /etc/rig/role } " ) "
case " $MARKER_LINE " in
2026-07-18 16:59:43 +00:00
"" | "role=control-plane" | "role=control-plane " *) ; ;
2026-07-18 14:50:46 +00:00
*) warn " this box's role marker says ' ${ MARKER_LINE } ' — not a control-plane box. Coolify belongs on role control-plane; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane' first so the marker tells the truth. " ; ;
esac
2026-07-10 20:50:41 +00:00
[ " $( id -u) " -eq 0 ] || die "must run as root"
export AUTOUPDATE = false
log " installing coolify ${ VERSION } (AUTOUPDATE=false) "
curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
bash /tmp/coolify-install.sh " $VERSION "
log " coolify ${ VERSION } installed with AUTOUPDATE=false "
feat(coolify): install the control-plane dump as a systemd timer
The Coolify control-plane database holds the GitHub App private key, every
registered server's SSH key, and every environment value for every environment
it manages. Backing it up was a manual runbook step, and the dump script lived
in cast — the off-box tool, whose src never references it. It runs on the box,
as root, under a scheduler: that is rig's job description.
It matters beyond tidiness. The dump is forensics, not a restore path — a lost
control plane is rebuilt fresh and reconciled from the manifest. So there will
be a next control-plane box, and as a runbook step it was born un-backed-up,
depending on someone remembering mid-incident. Now it is backed up from birth.
rig installs the machinery and templates /etc/coolify-dump.env empty at 0600,
never reading it back — no credential passes through rig. The script's own
guards make an unfilled file fail the unit loudly rather than ship plaintext.
systemd timer over cron: EnvironmentFile is the right idiom for 0600 secrets,
failures surface in systemctl status instead of being mailed into the void, and
Persistent=true catches a run missed while the box was down.
Two hazards the cast script missed, carried into the unit:
- aws-cli >= 2.23 enables default upload checksums that S3-compatible backends
reject; Debian 13 ships 2.23.6, so the unit defaults both checksum knobs to
when_required.
- A failed pg_dump piped into age still yields a valid, tiny, encrypted file
that uploads cleanly every night and looks exactly like a working backup. The
script now refuses to upload an empty artifact.
Closes #8
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:14:07 +00:00
log "next: rig coolify backup install (nightly control-plane dump — do this before the box holds anything)"
log "then: your bootstrap runbook (admin user, API token, GitHub App, S3 destination)"