Skip to content

Latest commit

 

History

History
195 lines (140 loc) · 6.05 KB

File metadata and controls

195 lines (140 loc) · 6.05 KB

Backup And Restore

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.


TL;DR

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

Postgres mode — backup

Create a local backup directory:

mkdir -p backups

Load environment values from .env:

set -a
. ./.env
set +a

Dump 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.

Postgres mode — restore

Start Postgres before restoring:

docker compose -f compose.prod.yml up -d postgres

Restore 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 -d

SQLite mode — backup

The 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.

Ad-hoc snapshot (transactional, safe while the app is running)

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.db

VACUUM 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).

Continuous replication with Litestream (recommended for production)

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-stopped

litestream.yml:

dbs:
  - path: /data/cms.db
    replicas:
      - type: s3
        bucket: my-cms-backups
        path: cms.db
        region: us-east-1

With 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 -d

SQLite mode — restore (ad-hoc snapshots)

If 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 -d

Restore uploads exactly as in Postgres mode.

Hosted Provider Backups

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.

Related

  • deployment/README.md — deployment overview
  • railway.md — Railway volume paths
  • vps.md — VPS Compose volume names
  • compose.prod.yml — Postgres and uploads volume names
  • compose.sqlite.yml — SQLite data volume