Backups
Traefik configuration snapshots and PostgreSQL off-host logical backups.
Backups
ByteBite has two distinct backup layers on BYTEBITE-APP-01:
- Traefik configuration snapshots — local-only rollback points for ingress/TLS
- PostgreSQL logical backups — automated, encrypted, off-host via Restic and Backblaze B2
Backup paths and PostgreSQL off-host restore verified: 2026-09-08
Traefik snapshots and PostgreSQL backups serve different purposes. Traefik backups do not cover database data. PostgreSQL off-host backups do not cover Traefik configuration or full VM state.
A. Traefik snapshots (local only)
Two recovery snapshots exist on the APP VM under /root/:
Pre-wildcard backup
/root/bytebite-traefik-backup-20260908-172700/Contains:
- Traefik static configuration
- Original
acme.json - Dokploy dynamic files
- Container inspect output
- Image inspect output
- Docker network inspect output
- Checksums
Post-wildcard backup
/root/bytebite-traefik-post-wildcard-20260908-175333/Contains current working wildcard-era configuration:
traefik.ymlacme.jsonacme-cloudflare.json- Container inspect output
- Checksums
Purpose
These snapshots are configuration rollback points for Traefik/TLS recovery. They are useful when:
- Wildcard TLS breaks after a config change
- Dokploy recreates
dokploy-traefikwithout custom wiring - ACME state needs restoration
Security of ACME material
Backup directories contain acme.json and acme-cloudflare.json with Let's Encrypt account keys and certificates. Treat backups as sensitive:
- Restrict access to root / authorized operators
- Do not copy to unsecured locations
- Do not commit to Git
Traefik snapshot limitations
| Limitation | Impact |
|---|---|
| Stored on same VM as production services | Lost if APP VM or storage fails |
| No automated schedule documented | Manual snapshot only |
| No PostgreSQL data | Database loss is not recoverable from these backups |
| No application container state | Only Traefik/infrastructure config |
Traefik snapshots alone are not sufficient for full disaster recovery.
B. PostgreSQL logical backups (off-host)
| Aspect | Status |
|---|---|
| Off-host backup | Implemented (Backblaze B2 via Restic) |
| Backup encryption | Implemented (Restic client-side) |
| Automated daily schedule | Implemented (systemd timer) |
| Retention / maintenance | Implemented (weekly systemd timer) |
| Off-host restore verified | Verified (2026-09-08) |
| Scheduled restore drill | Not implemented — manual script only |
| Production backup | Not applicable — production not deployed |
Architecture
PostgreSQL backups are logical dumps, not raw Docker volume copies.
PostgreSQL 18 (Dokploy-managed staging container)
|
| pg_dump --format=custom --no-owner --no-acl
v
temporary root-only local dump
/var/lib/bytebite-backups/postgres/staging/bytebite.dump
|
| Restic (encrypted)
v
encrypted off-host Restic repository
|
v
Backblaze B2 (private, S3-compatible API)Why logical pg_dump (not volume copy)
| Reason | Detail |
|---|---|
| Online consistency | Consistent logical backup while PostgreSQL remains running |
| Portability | Restore to any compatible PostgreSQL 18 instance |
| Independence from Docker layout | Does not depend on Dokploy volume paths or generated container names |
| Ownership neutrality | --no-owner and --no-acl avoid tying restore to original DB roles |
| Validation | pg_restore --list can verify dump integrity before upload |
Raw Docker volume backup is not the canonical PostgreSQL disaster-recovery method.
Dump format
pg_dump --format=custom --no-owner --no-aclOutput path (temporary, during backup run only):
/var/lib/bytebite-backups/postgres/staging/bytebite.dumpAfter a successful automated backup, this file is deleted. The staging backup directory normally contains no persistent dump.
Backblaze B2 and Restic
| Component | Role |
|---|---|
| Backblaze B2 | Off-host backup destination (private bucket) |
| Restic | Encrypted backup repository layer; uses B2 S3-compatible API |
Restic snapshot tag for staging PostgreSQL backups:
bytebite-postgres-stagingDo not confuse use cases: B2 is implemented for encrypted PostgreSQL backup storage. B2 for general application media/object storage remains planned — see Storage.
Credentials and configuration
Backup credentials are stored on BYTEBITE-APP-01 under /etc/bytebite/backups/:
| File | Purpose |
|---|---|
/etc/bytebite/backups/restic.env | Restic repository target, credential environment, password-file reference |
/etc/bytebite/backups/restic-password | Restic repository encryption password |
Both files are root-owned with restrictive permissions. Never document or reproduce their contents.
The Restic repository password must also exist in a secure off-host credential manager. Losing both the VM and that password makes encrypted backups unrecoverable from B2 alone.
Restic cache directory:
/var/cache/bytebite/resticSee Secrets for the credential inventory.
Shared backup lock
All PostgreSQL backup workflows acquire an exclusive flock on:
/run/lock/bytebite-postgres-backup.lockUsed by:
- Daily backup (
postgres-backup.sh) - Weekly maintenance (
postgres-backup-maintenance.sh) - Manual restore drill (
postgres-restore-drill.sh)
If the lock cannot be acquired, scripts abort rather than overlapping operations. This is intentional safety behavior.
Backup script
/usr/local/lib/bytebite/postgres-backup.shRoot-owned, umask 077. Behavior:
- Acquire exclusive flock on
/run/lock/bytebite-postgres-backup.lock - Find the currently running ByteBite staging PostgreSQL 18 container dynamically — does not rely on Dokploy-generated suffixes (e.g.
bytebite-staging-postgres-vmg94p.1.z4anjftsr84p1xhkvx6hz8jneis generated and unstable) - Verify
POSTGRES_DB=bytebiteandPOSTGRES_USER=bytebitewithout printing passwords - Create logical dump at
/var/lib/bytebite-backups/postgres/staging/bytebite.dump - Validate: non-empty dump,
pg_restore --listparses successfully, SHA-256 logged - Upload to Restic/B2 with tag
bytebite-postgres-staging - List latest matching off-host snapshot
- Delete local dump on exit (success or failure)
If multiple matching PostgreSQL containers are found, the backup aborts instead of guessing.
Automated backup (systemd)
| Unit | Schedule |
|---|---|
bytebite-postgres-backup.service | Type=oneshot — runs on timer trigger |
bytebite-postgres-backup.timer | Daily at 02:15 UTC |
Timer configuration includes Persistent=true: if the VM was down at the scheduled time, systemd can trigger a missed run after the machine becomes available again.
Both timer and service are enabled.
Type=oneshot behavior: After a successful run, systemctl status bytebite-postgres-backup.service shows Active: inactive (dead). This is normal — success is indicated by the service finishing/deactivating successfully, not by a continuously running process.
Retention and maintenance
| Unit | Schedule |
|---|---|
bytebite-postgres-backup-maintenance.service | Type=oneshot — runs on timer trigger |
bytebite-postgres-backup-maintenance.timer | Sunday at 03:15 UTC |
Script: /usr/local/lib/bytebite/postgres-backup-maintenance.sh
Staging retention policy (applied to snapshots tagged bytebite-postgres-staging):
| Policy | Keep |
|---|---|
| Daily | 14 |
| Weekly | 8 |
| Monthly | 3 |
This is the staging policy. Do not assume it automatically becomes the production policy.
Maintenance performs:
restic forgetwith staging tag and retention policyrestic prunerestic check
Do not routinely run restic prune manually outside this maintenance workflow unless troubleshooting requires it.
Local directory hierarchy
After cleanup and successful automated runs:
/var/lib/bytebite-backups/
/var/lib/bytebite-backups/postgres/
/var/lib/bytebite-backups/postgres/staging/No persistent bytebite.dump remains after a successful automated backup.
Restore verification (2026-09-08)
Off-host PostgreSQL backup and restore was verified on 2026-09-08:
- Real ByteBite PostgreSQL logical dump created (
pg_dumpcustom format) - Dump validated with
pg_restore --list - Dump uploaded to B2 through Restic
- Restic repository passed integrity check
- Dump restored from B2 — byte-identical to original
- Isolated PostgreSQL 18 container started with
--network none, no host port published - Clean restore database created;
pg_restorecompleted successfully - Core tables verified:
restaurants,restaurant_domains - Application data present; staging tenant domains verified (
luigi.staging.getbytebite.co,mario.staging.getbytebite.co) - Temporary restore container and storage cleaned up
The live staging PostgreSQL database was never modified during restore testing.
Restore drill script (manual)
/usr/local/lib/bytebite/postgres-restore-drill.shSuccessfully executed manually. Not scheduled by a systemd timer.
Behavior:
- Acquire flock on
/run/lock/bytebite-postgres-backup.lock - Load Restic configuration from
/etc/bytebite/backups/ - Restore latest snapshot tagged
bytebite-postgres-stagingto a temporary work directory under/var/lib/bytebite-backups/ - Validate restored
bytebite.dumparchive - Start isolated
postgres:18container with--network none, no host ports - Create clean database
bytebite_restore - Run
pg_restore - Verify
restaurantsandrestaurant_domainstables, non-zero data, Luigi and Mario staging domains - Delete temporary container and work directory
Expected terminal result: RESTORE DRILL VERIFIED
The live staging database is never targeted by the restore drill.
Current limitations
| Limitation | Status |
|---|---|
| Scheduled automatic restore drill | Not implemented |
| Backup failure alerting | Not implemented |
| Production backup automation | Not deployed (production not live) |
| Traefik config off-host backup | Not implemented — local snapshots only |
| Full APP VM disaster recovery | Incomplete — DB restore verified; host rebuild not |
Related
- Database — PostgreSQL service details
- Disaster recovery — recovery runbooks
- Validation — backup health checks
- Operations — recurring backup tasks
- Secrets — Restic and B2 credential inventory
- Known gaps — remaining backup/DR gaps
- TLS — ACME state files