A complete backup includes the database and the uploaded media. The procedure depends on whether you're using Postgres or SQLite as the database engine. Pick the matching section below.
| Deployment | Database backup | Upload backup |
|---|---|---|
| VPS SQLite Compose | Copy /app/data/cms.db from the data volume |
Archive the uploads volume |
| VPS Postgres Compose | pg_dump from the postgres service |
Archive the uploads volume |
| Railway SQLite | Back up the app volume mounted at /app/storage |
Same app volume, under /app/storage/uploads |
| Railway Postgres | Back up the Postgres service volume/database | Back up the app volume mounted at /app/storage |
Create a local backup directory:
mkdir -p backupsLoad environment values from .env:
set -a
. ./.env
set +aDump Postgres:
docker compose -f compose.prod.yml exec -T postgres \
pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" \
> "backups/instatic-$(date +%F).sql"Archive uploads:
docker run --rm \
-v instatic-prod_uploads:/uploads:ro \
-v "$PWD/backups:/backup" \
alpine \
tar czf "/backup/instatic-uploads-$(date +%F).tgz" -C /uploads .If your Compose project name is not instatic-prod, find the actual uploads volume name with docker volume ls | grep uploads.
Start Postgres before restoring:
docker compose -f compose.prod.yml up -d postgresRestore the database:
set -a
. ./.env
set +a
cat backups/instatic-YYYY-MM-DD.sql | docker compose -f compose.prod.yml exec -T postgres \
psql -U "$POSTGRES_USER" "$POSTGRES_DB"Restore uploads:
docker run --rm \
-v instatic-prod_uploads:/uploads \
-v "$PWD/backups:/backup" \
alpine \
sh -lc "rm -rf /uploads/* && tar xzf /backup/instatic-uploads-YYYY-MM-DD.tgz -C /uploads"Then start the full stack:
docker compose -f compose.prod.yml up -dThe compose.sqlite.yml override stores the SQLite database in the data named volume at /app/data/cms.db. Both ad-hoc and continuous strategies are documented below.
Use Bun (already in the app container) and SQLite's online backup API to capture a consistent snapshot without stopping the CMS:
docker compose -f compose.prod.yml -f compose.sqlite.yml exec app \
bun -e "import { Database } from 'bun:sqlite'; const src = new Database('/app/data/cms.db', { readonly: true }); src.exec(\"VACUUM INTO '/app/data/snapshot.db'\");"
docker compose -f compose.prod.yml -f compose.sqlite.yml cp \
app:/app/data/snapshot.db "./backups/instatic-$(date +%F).db"
docker compose -f compose.prod.yml -f compose.sqlite.yml exec app \
rm /app/data/snapshot.dbVACUUM INTO writes a fully consistent copy of the database to a new file — safe to run live, no locking required. Then docker compose cp exports it to the host.
Archive uploads exactly the same way as the Postgres mode (the uploads volume is shared between the two modes).
Litestream replicates a SQLite database to S3-compatible object storage with second-level RPO. Add a sidecar to the SQLite stack:
# Append to compose.sqlite.yml under `services:`
litestream:
image: litestream/litestream:latest
command: replicate
volumes:
- data:/data:ro
- ./litestream.yml:/etc/litestream.yml:ro
environment:
LITESTREAM_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?Set S3 access key in .env}
LITESTREAM_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?Set S3 secret key in .env}
depends_on:
- app
restart: unless-stoppedlitestream.yml:
dbs:
- path: /data/cms.db
replicas:
- type: s3
bucket: my-cms-backups
path: cms.db
region: us-east-1With Litestream running, every write to cms.db is shipped to S3 within seconds. To restore, point Litestream at the S3 backup before starting the app:
docker run --rm \
-v instatic-prod_data:/data \
-e LITESTREAM_ACCESS_KEY_ID -e LITESTREAM_SECRET_ACCESS_KEY \
-v "$PWD/litestream.yml:/etc/litestream.yml:ro" \
litestream/litestream:latest \
restore -o /data/cms.db /data/cms.db
docker compose -f compose.prod.yml -f compose.sqlite.yml up -dIf you took a VACUUM INTO snapshot rather than running Litestream:
# Stop the app first — restoring overwrites the live DB.
docker compose -f compose.prod.yml -f compose.sqlite.yml stop app
# Copy the backup file into the data volume (replacing the existing DB).
docker compose -f compose.prod.yml -f compose.sqlite.yml run --rm --no-deps \
--entrypoint "" app sh -lc "rm -f /app/data/cms.db /app/data/cms.db-wal /app/data/cms.db-shm"
docker compose -f compose.prod.yml -f compose.sqlite.yml cp \
"./backups/instatic-YYYY-MM-DD.db" app:/app/data/cms.db
# Start the app — the WAL/SHM sidecar files will be regenerated on next open.
docker compose -f compose.prod.yml -f compose.sqlite.yml up -dRestore uploads exactly as in Postgres mode.
When Instatic runs on a provider that offers managed Postgres (Railway Postgres, RDS, Supabase, Render Postgres, Fly Postgres, etc.), the provider's snapshot, volume backup, or point-in-time tooling is the recommended first backup path. Keep an independent pg_dump schedule when you need provider-independent recovery.
Railway-specific paths:
| Template | Database path | Upload path |
|---|---|---|
| SQLite | /app/storage/data/cms.db |
/app/storage/uploads |
| Postgres | Railway Postgres service | /app/storage/uploads |
For uploads, back up whatever disk or volume is mounted at UPLOADS_DIR.
- deployment/README.md — deployment overview
- railway.md — Railway volume paths
- vps.md — VPS Compose volume names
compose.prod.yml— Postgres and uploads volume namescompose.sqlite.yml— SQLite data volume