Skip to main content
A Compose install keeps its data in Docker volumes. Three of them hold what no reinstall brings back, and those are what to back up before every upgrade.

What to back up

Keep your .env file and your edited docker-compose.yaml too. They are ordinary files in the folder you cloned, and .env holds JWT_SECRET and ENCRYPTION_KEY: a database restored without the same ENCRYPTION_KEY cannot read its stored secrets. Compose prefixes each volume with the project name, the folder you cloned into, so the full names are usually postqueen-docker-compose_postgres-volume and so on. docker volume ls shows them.

Back up

Stop the stack first, so PostgreSQL is not writing while you copy its files.
docker compose down stops every service in the project. On a host where other services share that project, stop the PostQueen services by name instead: docker compose stop postqueen postqueen-postgres temporal temporal-postgresql.
Then archive each volume with a throwaway container that mounts it at /data and the current folder at /backup:
Copy the archives off the server. A backup on the same disk is lost with the disk.

Restore

Replace a volume rather than unpacking over a used one, so nothing of the old contents stays underneath:
Repeat for the other volumes. Restore all three from the same backup, so the schedule in Temporal matches the posts in the database.
A PostgreSQL data folder opens only under the major version that wrote it. The Compose file runs postgres:17-alpine for PostQueen and postgres:16 for Temporal: restore each archive under the same image.
A backup you have never restored is a guess. Try a restore on a spare machine before you need one.

If your media is in R2

With Cloudflare R2, postqueen-uploads stays empty and the bucket holds the media. The database stores only each file’s address, so back up the bucket and the database together and restore them from the same moment: a database from Monday next to a bucket from Friday leaves the posts in between with missing images, and nothing reports an error. Copy the bucket to a second bucket, for example with rclone:
Use copy, not sync: a file deleted from the live bucket should stay in the backup. Give the backup job its own R2 token, allowed to read the media bucket and write the backup bucket, so it cannot delete the media it protects.

Upgrade

1

Back up

Back up postgres-volume and temporal-postgres-data as above. The next step can change the database.
2

Move the image forward

With a pinned tag, raise it in docker-compose.yaml to a newer tag from the app’s tags:
With latest, nothing changes in the file.
3

Pull and recreate

up -d recreates the containers whose image changed. Watch the log until Backend started successfully on port 3000.
Only move forward. Never start an image older than the one that last ran against the database: its schema step can drop the columns the newer release added.

Where the schema changes

You never run a database migration by hand. Every time the container starts, it applies the image’s Prisma schema before starting the app. The variable PRISMA_MIGRATE decides how: db push makes the database match the schema, and --accept-data-loss lets it drop a column or table the schema no longer has, with its contents, without asking. That is why a backup comes before every upgrade, and why an older image is dangerous. The match is exact text, so no, off or false with a trailing space select migrate deploy. migrate deploy applies the repository’s migrations in order instead, a 0_init baseline and one per later schema change, and never drops what a migration does not say to. A database built by db push has no record of those migrations, so switching takes one step: with the current image running, and so the database matching its schema, mark every migration that image ships as applied, then set the variable and recreate the container.
The loop takes every migration folder and skips migration_lock.toml, which is a file. Run it before you move the tag, on the image that is running now. If a new image already fails to start, set the tag back to your previous one, run the loop there, and only then move forward.
Marking only 0_init is not enough on a database that already has later changes: migrate deploy would then try to add columns that exist and stop the start. Back up first either way.
Stuck on v3.6.121? That release ran db push without --accept-data-loss, so an install that came from a release older than v3.6.107 stopped at its schema step on every start and restarted in a loop. Move the tag to v3.6.122 or newer, or latest, and recreate the container.

The command that deletes everything

docker compose down -v deletes the volumes with the containers: the database, the uploads and the scheduled work, with no prompt and no undo. Plain docker compose down keeps every volume. For a settings change, recreate the one container instead: see Docker Compose.
Last modified on October 9, 2026