Back Up and Restore
A Deckyard installation keeps its state in two places: the database and the uploaded media. Back up both, and rehearse a restore once before you need it. A back-up that was never restored is a hope, not a back-up.
| Where | What | Back up? |
|---|---|---|
| PostgreSQL | Everything except media files: decks, versions, themes, users, organizations, settings, comments, analytics. | Yes |
server/uploads/ (a volume) or the S3 bucket | Uploaded images and files. The database refers to them by key. | Yes |
server/data/private-media/ (a volume) or the bucket | Uploaded font variants (in S3 mode under private/ in the bucket). The database refers to them. | Yes |
server/data/deck-thumbs/ | Deck thumbnails, a cache rebuilt on demand. | No |
| The container image | Rebuilt from the repository at the version you run. | No |
Your .env, or the platform’s environment, holds secrets rather than state. It belongs in your secret store, not next to the dump. Keep AUTH_SECRET the same after a restore, or everyone is signed out.
Back up
Section titled “Back up”The database, as a custom-format dump (compressed, and pg_restore can pick from it). On the compose stack:
docker compose exec -T postgres sh -c \ 'pg_dump -Fc --no-owner --no-acl -U "$POSTGRES_USER" "$POSTGRES_DB"' \ > deckyard-$(date +%F).dumpAgainst any other PostgreSQL, run pg_dump -Fc --no-owner --no-acl "$DATABASE_URL" > deckyard-$(date +%F).dump with a client whose major version is at least the server’s. --no-owner --no-acl makes the dump restorable under a different role name, which is what a restore onto a new platform needs.
The media, right after the dump:
tar czf deckyard-uploads-$(date +%F).tgz server/uploads server/data/private-media # volumes or bind mountsaws s3 sync s3://<bucket> ./deckyard-media-$(date +%F) # S3 mode, or your provider's own toolA dump taken while people edit is consistent in itself. Media copied a minute later may hold a file the dump does not know yet, which is harmless. The other way round, a dump that refers to a file you did not copy, shows up as a missing image. So copy the media after the dump.
Restore
Section titled “Restore”Restore into an empty database, then let the migrations bring the schema to the version you run:
docker compose stop appdocker compose exec -T postgres sh -c \ 'dropdb -U "$POSTGRES_USER" --if-exists "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'docker compose exec -T postgres sh -c \ 'pg_restore --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \ < deckyard-2026-09-29.dumptar xzf deckyard-uploads-2026-09-29.tgz && sudo chown -R 1000:1000 server/uploads server/datadocker compose start app # the entrypoint applies pending migrationsdocker compose exec app node scripts/doctor.jsOutside compose it is the same four steps: an empty database, pg_restore --no-owner --no-acl -d "$DATABASE_URL" <dump>, npm run db:migrate (the container does this at start), and npm run doctor. A dump from an older release restores into a newer one this way. The reverse, a dump from a newer release into an older version, is not supported.
Rehearse it once
Section titled “Rehearse it once”Restore into a throwaway database next to the real one and check the result, without touching production:
docker run -d --name deckyard-restore-test -p 55432:5432 \ -e POSTGRES_USER=deckyard -e POSTGRES_PASSWORD=restore -e POSTGRES_DB=deckyard postgres:16-alpinedocker exec -i deckyard-restore-test pg_restore --no-owner --no-acl -U deckyard -d deckyard < deckyard-2026-09-29.dumpexport DATABASE_URL=postgres://deckyard:restore@localhost:55432/deckyard DATABASE_SSL=falsenpm run db:migrate && npm run doctor # in a checkout at the version you rundocker exec deckyard-restore-test psql -U deckyard -d deckyard -Atc 'select count(*) from presentations'docker rm -f deckyard-restore-testThe doctor should show Database and Migrations green, and the deck count should match production. To see it with your own eyes, start the server against it (PORT=4199 npm run start) and open a deck.
On a PaaS
Section titled “On a PaaS”- Coolify can schedule dumps of a PostgreSQL resource (in the database’s back-up settings), kept on the server or sent to an S3 destination. That covers the database; the media volumes are not in it, so copy them separately or keep media in S3.
- Managed PostgreSQL (Railway, Render, Fly, a cloud provider) takes its own snapshots. Check the retention, and still take an occasional
pg_dump: a snapshot restores only on that provider, a dump restores anywhere. - Volumes are rarely in the platform’s back-up. Media in S3 with bucket versioning is the simpler guarantee on a PaaS.
Whatever the platform, a restore ends the same way: migrations, then the doctor.