feat(coolify): install the control-plane dump as a systemd timer #9
No reviewers
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#9
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/coolify-backup-timer"
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?
Closes #8.
rig coolify installused to end by handing the nightly control-plane dump to a humanrunbook. Some of what it hands off genuinely is runbook work — an admin user and an API
token are console acts against a running Coolify. The dump isn't: it runs on the box, as
root, under a scheduler, which is rig's job description verbatim.
What lands
Installs
age+awscli,/usr/local/sbin/coolify-dump.sh, a systemd service + timer, andan empty
0600bindings file at/etc/coolify-dump.env. Convergent — a bindings filethat already exists is never touched.
The dump script is re-homed out of
cast, where it was a stowaway:castis the off-boxtool and its
src/never references the script. Its first line isdocker exec coolify-db pg_dump …, which only runs where the Coolify containers are.The seam: rig installs machinery, never credentials
Per the README's own philosophy — "stores no credential, ever":
age+awscliAGE_RECIPIENT(a public key)S3_BUCKET,S3_ENDPOINT0600bindings filerig writes that file blank and never reads it back. The script's guards
(
: "${AGE_RECIPIENT:?…}") make an unfilled file fail the unit loudly rather than shipplaintext or a zero-byte artifact. A silent backup is worse than a missing one.
Why a timer, not cron
EnvironmentFile=is the correct idiom for0600secrets (noset -asourcing wrapper),failures surface in
systemctl status/list-timersrather than being mailed into a voidwith no MTA, and
Persistent=truecatches a run missed while the box was down. Cron givesnone of that and needs a wrapper script just to load the env.
Two hazards the
castscript missedaws-cli >= 2.23breaks against S3-compatible backends. 2.23 turned on new defaultupload checksums that Hetzner/MinIO/Ceph reject — and Debian 13 ships
awscli2.23.6,so this is the default install, not an edge case. The unit defaults both checksum knobs to
when_required.pg_dumppiped intoagestill produces a valid, tiny, encrypted file. Itwould upload cleanly every night and look exactly like a working backup. The script now
refuses to upload an empty artifact.
Why this is a correctness fix, not tidiness
The control-plane dump is explicitly forensics, not a restore path: a lost control plane is
rebuilt fresh and reconciled from its manifest. So there will be a next control-plane
box — that is the design. As a runbook step, every rebuilt control plane was born
un-backed-up, depending on someone remembering it mid-incident. Now it's backed up from
birth, and
rig coolify installpoints at it as the next step.Testing
bash test/cli.sh→ 35 passed, 0 failed (27 existing + 8 new: dispatch, bad subcommand,--help, flag-needs-value ×2, unknown flag, non-root refusal). shellcheck clean. Thegenerated systemd unit was rendered and inspected.
Not covered, deliberately: rig cannot verify an upload actually works — that needs real
credentials. The command prints the manual gate (
systemctl start coolify-dump.service, thenconfirm the object is in the bucket and is really
ageciphertext). Reading a backup backshould stay the operator's job.
🤖 Generated with Claude Code