Self-Hosted Backup Strategy
Coolify’s built-in backup UI is limited for compose-stack setups. Here’s what actually works.
Coolify built-in backup — limitations
Coolify’s backup tab works reliably only for databases deployed as Standalone Databases via Coolify’s Databases section.
For databases inside Docker Compose stacks (the default for one-click services like Listmonk, Twenty CRM, n8n, Plausible, etc.), the backup tab may not appear at all, or may show “Success” while silently failing to upload to S3.
Postgres backups — PGBackWeb
PGBackWeb is available as a Coolify one-click service. It connects directly to any Postgres container on the VPS and uploads backups to S3-compatible storage.
Setup:
- Deploy PGBackWeb via Coolify one-click
- For each compose-stack Postgres container, connect it to the PGBackWeb network:
docker network connect <SERVICE_UUID> <pgbackweb_container_name>
- In the PGBackWeb UI, add each database using the internal container name as the host
- Set up a schedule (e.g. daily at 02:00) and an S3 destination
⚠️ If PGBackWeb is ever redeployed, the network connections are lost and must be re-run.
File and SQLite backups — docker-volume-backup
Services like Vaultwarden and Uptime Kuma use file/SQLite storage — PGBackWeb can’t handle these. Use the offen/docker-volume-backup sidecar:
backup:
image: offen/docker-volume-backup:v2
environment:
BACKUP_CRON_EXPRESSION: "0 2 * * *"
AWS_S3_BUCKET_NAME: your-backup-bucket
AWS_S3_PATH: vaultwarden/
AWS_ENDPOINT: <R2_ENDPOINT_HOSTNAME> # bare hostname, NO https://
AWS_ACCESS_KEY_ID: <your_key>
AWS_SECRET_ACCESS_KEY: <your_secret>
AWS_S3_FORCE_PATH_STYLE: "true"
BACKUP_RETENTION_DAYS: "14"
volumes:
- vaultwarden-data:/backup/vaultwarden-data:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
Add this as a sidecar in the service’s docker-compose.yml via SSH (not via Coolify UI — it will be overwritten if you redeploy through Coolify).
⚠️
AWS_ENDPOINTmust be a bare hostname only — nohttps://prefix.
Healthcheck variable substitution gotcha
${VARIABLE} in Docker healthcheck test commands is substituted by the host shell before being passed to Docker. If the variable is not exported in the host environment, it becomes empty.
# Safe — no variable substitution
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
# Risky — ${POSTGRES_DB} may be empty at host level
healthcheck:
test: ["CMD-SHELL", "pg_isready -d ${POSTGRES_DB}"]
Verify every backup manually
After setting up any backup configuration, always confirm the file actually appears in your S3/R2 bucket. Don’t trust UI success messages alone — especially with Coolify + R2, where the upload can silently fail even after a “Success” is shown.
Zenith Stack
We set up and maintain backup strategies as part of our infrastructure service.
Learn more