Control-plane backup plumbing belongs in rig, not the runbook (nightly Coolify dump) #8
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:bootstrap
scope:coolify
scope:db
scope:docs
scope:drill
scope:installer
scope:labels
scope:platform
scope:runner
scope:users
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/rig#8
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
rig coolify installends by printing:Some of what it hands off genuinely is runbook work — creating an admin user and minting
an API token are console acts against a running Coolify. But one item on that list is not:
the nightly control-plane dump is on-box root plumbing, and it belongs in rig.
What's missing
The Coolify control plane's own Postgres holds the GitHub App private key, every registered
server's SSH key, and every environment variable value for every environment it manages.
Losing it un-backed-up loses the fleet's secrets. Backing it up requires, on the box:
ageand an S3 client installedToday all four are a manual runbook. That is the gap.
Why it's rig's job, not the runbook's
It is exactly rig's job description. On-box, as root, on a pristine Debian server —
installing packages and a systemd unit. rig already does this one door down:
rig runner installcreates a user, installs a versioned binary, and registers a systemd service.A timer for the dump is the same move.
The DR story makes it a correctness bug, not tidiness. The control-plane dump is
explicitly 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 — that is the design.
If the backup plumbing is a manual runbook step, every rebuilt control plane is born
un-backed-up, and the person rebuilding it (stressed, mid-incident) has to remember.
In rig, a control-plane box is backed up from birth.
The script is currently in the wrong repo. It ships in
castasscripts/dump-coolify-db.sh— butcastisthe off-box tool, and
cast'ssrc/never references it. It is payload, and its first lineis
docker exec coolify-db pg_dump …, which only runs where the Coolify containers are. It'sa stowaway from before the rig/cast/infra split re-homed the code. It should live here.
The seam — rig installs machinery, never credentials
rig's philosophy (README) is "plumbing logic only — no hostnames, no bindings, no secrets,
nothing about your infrastructure... stores no credential, ever." That holds cleanly here:
age+awscliAGE_RECIPIENT(a public key)S3_BUCKET,S3_ENDPOINTAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/ region0600env-file templaterig writes the template with blank values and never reads it back. The dump script's own
guards (
: "${AGE_RECIPIENT:?…}") mean an unfilled file fails the unit loudly rather thanshipping plaintext or a zero-byte artifact — the right failure mode for a backup.
Proposal
New convergent subcommand, so the plumbing can be (re)installed on an existing control plane
without touching Coolify itself:
systemd timer, not cron.
EnvironmentFile=is the correct idiom for0600secrets (noset -asourcing wrapper), failures are visible insystemctl status/list-timersratherthan mailed into the void, and
Persistent=truecatches a run missed while the box was down.Cron gives none of that and needs a wrapper script to load the env at all.
Two things to carry over that the current
castscript gets wrong or omits:aws-cli ≥ 2.23breaks against S3-compatible backends. Debian 13 (trixie) shipsawscli 2.23.6, and 2.23 enabled new default upload checksums that Hetzner/MinIO/Cephreject. The unit must default
AWS_REQUEST_CHECKSUM_CALCULATION=when_requiredandAWS_RESPONSE_CHECKSUM_VALIDATION=when_required.pg_dumppiped intoagestill produces avalid, tiny, encrypted file. Without a size check that uploads cleanly every night and looks
exactly like a working backup.
Out of scope
rig cannot verify the upload actually works — that needs real credentials. The command prints
the manual verification (fill the env file →
systemctl start coolify-dump.service→ confirmthe object is in the bucket and really is
ageciphertext). Reading a backup back is theoperator's gate, and should stay one.