How to replace a repo's bespoke backup container with BackupHelper. The goal: every consuming repo keeps only a ~20-line meta-Dockerfile and moves its backup configuration into BACKUP_CONFIG_JSON (or a mounted file), so backup logic is maintained once, centrally. See deployment for the meta-Dockerfile pattern and plugins for app-specific sources.
The fleet's backup sidecars drifted: some sign webhooks with HMAC, others send the secret in plaintext; several have no integrity manifest; only one does retry/backoff. Consolidating onto one image makes every repo a strict superset and fixes those inconsistencies in one move — while application-aware logic stays in each repo via a Source plugin or lifecycle hook.
Each repo keeps its Dockerfile, reduced to a meta-layer:
FROM ghcr.io/bauer-group/cs-backuphelper/backuphelper:latest
# ---------------------------------------------------------------------------
# Metadata Labels
# ---------------------------------------------------------------------------
LABEL vendor="BAUER GROUP"
LABEL maintainer="Karl Bauer <karl.bauer@bauer-group.com>"
LABEL org.opencontainers.image.title="CS-IAMStack Database-Backup"
LABEL org.opencontainers.image.source="https://github.com/bauer-group/CS-IAMStack"
LABEL org.opencontainers.image.version="1.0.0"
# Inherit ENTRYPOINT, USER, ENV, HEALTHCHECK from the base image.
# Sources / destinations / schedule come from env or BACKUP_CONFIG_JSON in compose.Track :latest so a new central-engine release flows into the consuming image on
its next build. The central image already ships the postgres/mariadb clients,
tini, a non-root user and the healthcheck — the meta-layer adds only labels (and,
if needed, an extra client via apk add).
The compose service points at that image and supplies the job config inline — see docker-compose.yml and docker-compose.sidecar.yml.
- Repoint the repo's backup Dockerfile
FROM ghcr.io/bauer-group/cs-backuphelper/backuphelper:latestand keep only OCI labels (+PG_CLIENT_VERSIONor extra clients if needed). Track:latestso a new engine release flows into the consuming image on its next build. - Move the backup config from the old env vars into
BACKUP_CONFIG_JSONin the compose service (secrets as$${VAR}). - Reconcile the repo's CI config. Deleting the old sidecar's
src/<backup>/sources leaves three config files pointing at paths that no longer exist — this is the step most easily forgotten, and it surfaces as red CI after the merge, not during it. Commit these asci(backup): …(notfix:, so semantic-release does not mint a release for a config-only reconciliation):.github/config/docker-base-image-monitor/base-images.json— the backup image now consumesbackuphelper:latest, notpython:*-alpine. Add abackuphelperentry (imageghcr.io/bauer-group/cs-backuphelper/backuphelper, taglatest, variableBACKUPHELPER_LATEST_DIGEST). Keep the existing python entry only if another component in the repo still buildsFROM python(e.g. IAM'sdirectory-sync) — then add alongside it; otherwise replace it. A stale-but-green monitor is the trap: it tracks an image nothing builds on and never fires on a real engine release..github/dependabot.yml— remove thepipecosystem entry for the backup dir. The migration deletesrequirements*.txt, so the pip watcher has no manifest and Dependabot errors with "no manifest found". Leave thedockerentry (it now inertly tracks the:latestbase) but correct any stalepython:*-alpinecomment.sonar-project.properties(only repos that run Sonar) —sonar.sources/sonar.testspointing at the deletedsrc/<backup>/{src,tests}fail Code Quality withsonar-scannerexit 3 ("folder does not exist"). Pointsonar.sourcesat what survives (e.g.scripts) and dropsonar.testsif no test folder remains.
docker compose run --rm backup --nowand confirm a snapshot + sidecar manifest appear;backuphelper verify <id>.- Confirm a restore into a staging target (
restore <id> --force) before decommissioning the old container. - For app-specific export/restore (n8n CLI, NocoDB REST), add a Source plugin in the meta-layer — see plugins.
Transient CI noise vs. real breakage. Two failures during a migration are not repo defects and self-heal on re-run: a first-pull
403 Forbiddenforbackuphelper:latestright after a fresh publish (the package is public — this is GHCR permission propagation), and a503from the Sonar scanner (the self-hosted SonarQube server momentarily down). Only a deterministic error tied to a deleted path — e.g.sonar-scannerexit 3 on a missingsonar.testsfolder — is yours to fix. Re-run before reaching for a code change.
Effort to migrate, and what each repo's meta-layer sets (the engine supplies everything else):
| Effort | Repo | Status — meta-layer sets |
|---|---|---|
| trivial | SaaS-Projects/CovalidaIAM |
✅ done — consumer, mirrors cs-iamstack/database-backup |
| low | Container-Solution/IAMStack |
✅ done — canonical meta-layer + consumer template |
| low | Container-Solution/IAM (Zitadel) |
✅ done — source=postgres(zitadel) |
| low | Production+Development/SonarQube |
✅ done — source=postgres; webhook plaintext → HMAC |
| low | Container-Solution/ZAMMAD |
✅ done — source=[postgres, filesystem:/opt/zammad/storage] |
| low | Demo-Projects/ContainerBackupPostgreSQL |
⏸ deferred — needs engine S3 Object-Lock/WORM (it is the donor) |
| medium | Container-Solution/Outline |
✅ done — source=[postgres, s3:attachments], dest=[local,s3] |
| medium | Container-Solution/DocumentSigning |
✅ done — source=[postgres(custom), s3:documents, env(47-var whitelist)], off-site S3 only; a documenso command plugin adds the ENCRYPTION_KEY restore gate + restore-env lost-key recovery; two-service split (one-shot + scheduler) kept |
| medium | Container-Solution/NocoDB |
✅ done — source=[postgres(plain), filesystem:/nocodb-data, nocodb-rest]; the REST exporter + restore-schema/records/attachments ship as the backuphelper-nocodb plugin (Source + nocodb command group) — needs engine ≥ v1.5.0 (CLI-command injection + per-source enabled) |
| medium | Container-Solution/WordPressStack |
✅ done — pure config: source=[mariadb:database, filesystem:uploads, filesystem:content(subdirs plugins/themes/languages)]; Teams MessageCard kept; no plugin |
| high | Container-Solution/n8n |
✅ done — meta image + nodejs/npm/n8n (pinned to the server version); a backuphelper-n8n plugin: n8n Source (CLI export, decrypted) + n8n command group (import-workflows w/ re-activation, import-credentials, export-now, restore-env) + a pre_restore ENCRYPTION_KEY gate. postgres[custom] drops execution row-data (exclude_table_data). Two alt restore paths: DB dump OR JSON merge |
| high | Internal-Projects/BAUERGROUP.HardwareIDAllocator |
.NET→Python re-platform or NDJSON mode |
Out of scope — not migrated onto BackupHelper, these stay standalone:
Internal-Projects/CanvaBackupRunner(standalone SaaS-export runner),Container-Solution/GitHubBackup(a GitHub-specific git/LFS/wiki engine, not a generic DB/file backup), and the Home AssistantS3CompatibleBackup(an HA plugin, not a container).
Start with the trivial/low tier (pure DB backups) to prove the model, then the medium tier (DB + files/objects), and finally the high tier where an app-specific exporter becomes a plugin.
Repos that migrate inherit capabilities their old sidecar lacked: a sha256 integrity manifest + verify, normalized HMAC-SHA256 webhook signing, retry/backoff on network calls, optional client-side encryption, count/age/GFS/smart retention, a functional healthcheck, and the full restore CLI.