Backups are a first-class concern in this stack. Data loss in Paperless-ngx or Vaultwarden would be severe and potentially irreversible.
| Layer | Method | Schedule | Location |
|---|---|---|---|
| Postgres database | pg_dump via cron |
Nightly | Local volume + offsite |
| Docker volumes | docker-volume-backup sidecar |
Nightly | Local volume + offsite |
| OCI boot volumes | OCI Block Volume Backup Policy | Weekly (Bronze) | OCI snapshot |
| Caddy certificates | Included in volume backup | Nightly | With volume backup |
A nightly pg_dump exports the Paperless database to a compressed file.
cd ~/homelab-cloud-stack/vm2/stack
docker compose exec postgres pg_dump \
-U ${POSTGRES_USER} \
-d ${POSTGRES_DB} \
| gzip > ~/backups/paperless_$(date +%Y%m%d).sql.gzAdd to the deploy user's crontab (crontab -e):
# Nightly Postgres backup at 02:00
0 2 * * * cd ~/homelab-cloud-stack/vm2/stack && \
docker compose exec -T postgres pg_dump \
-U paperless paperless \
| gzip > ~/backups/paperless_$(date +\%Y\%m\%d).sql.gzCreate the backup directory:
mkdir -p ~/backupsThe offen/docker-volume-backup image handles volume backup to a local or remote destination.
It is included as a service in vm2/stack/docker-compose.yml. Configure it via the environment variables in .env:
BACKUP_CRON_EXPRESSION=0 3 * * * # 03:00 nightly
BACKUP_RETENTION_DAYS=14 # keep 14 days of backupsVolume backups are stored in a separate backup_archive volume mounted at /archive inside the backup container.
To manually trigger a backup:
docker compose exec backup backupFor true durability, replicate backups offsite. Options:
OCI provides 20 GB of Object Storage on Always-Free. Use rclone to sync backups:
# Install rclone
sudo apt install -y rclone
# Configure OCI Object Storage as a remote
rclone config
# Choose: S3-compatible, use OCI endpoints
# Sync backup directory
rclone sync ~/backups oci-bucket:homelab-backups/Add to crontab after the pg_dump job:
30 2 * * * rclone sync ~/backups oci-bucket:homelab-backups/ --log-file ~/backups/rclone.log# Initialize restic repository
restic -r s3:https://<endpoint>/<bucket> init
# Backup volumes
restic -r s3:https://<endpoint>/<bucket> backup /var/lib/docker/volumes/
# Prune old snapshots (keep 30 days)
restic -r s3:https://<endpoint>/<bucket> forget --keep-daily 30 --pruneIn the OCI console, assign a backup policy to each VM's boot volume:
Block Storage → Boot Volumes → [VM boot volume] → Assign Backup Policy → Bronze
Bronze policy: weekly incremental, retained for 1 year. This provides a full-system restore point at the infrastructure level.
# On VM2
cd ~/homelab-cloud-stack/vm2/stack
# Stop the webserver and worker (not the database)
docker compose stop paperless-web paperless-worker
# Drop and recreate the database
docker compose exec postgres psql -U paperless -c "DROP DATABASE paperless;"
docker compose exec postgres psql -U paperless -c "CREATE DATABASE paperless;"
# Restore from dump
gunzip -c ~/backups/paperless_20250101.sql.gz \
| docker compose exec -T postgres psql -U paperless paperless
# Restart services
docker compose start paperless-web paperless-workerIf a volume was lost or corrupted:
# Stop the affected service
docker compose stop memos
# Remove the damaged volume
docker volume rm vm1_stack_memos_data
# Restore from backup tar (created by docker-volume-backup)
docker run --rm \
-v vm1_stack_memos_data:/volume \
-v ~/backups:/backup \
alpine tar xf /backup/memos_data_backup.tar.gz -C /volume
# Restart
docker compose start memos- In OCI Console: Block Storage → Boot Volume Backups → [backup] → Restore Boot Volume
- Launch a new instance with the restored boot volume
- Update DNS A records to the new instance's public IP
- Update the NSG SSH ingress source CIDR to your current IP if needed
Run restore tests periodically. At minimum:
- Monthly: Test a Postgres restore on a temporary VM
- Quarterly: Test a full volume restore
A backup that has never been tested is not a backup.