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./data and the current folder at /backup:
Restore
Replace a volume rather than unpacking over a used one, so nothing of the old contents stays underneath: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:
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 With
docker-compose.yaml to a newer tag from the app’s tags: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.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 variablePRISMA_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.
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.
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.