Skip to main content
Run docker volume ls after a Compose install and six volumes come back. Two of them you could delete tonight and never notice, and a third is empty until you put something in it. The remaining three carry what no reinstall brings back, so those are the ones worth your disk space and your attention.

What you cannot rebuild

A fourth volume, postqueen-config, is mounted at /config and on a stock install it is empty. The shipped image reads its configuration from the container environment and from /app/.env, and nothing in it looks at /config. Back it up only if you put a file there yourself and you know what reads it.

What rebuilds itself

postqueen-redis-data is a cache. temporal-elasticsearch-data is the search index behind the Temporal UI at http://localhost:8080, and the temporalio/auto-setup image sets that index up again when it starts against an empty cluster. Leave both out of your backup. One more thing lives outside Docker entirely. The directory you cloned holds your edited docker-compose.yaml and the dynamicconfig folder that Temporal bind-mounts from it. Those are ordinary files on the host, so whatever already backs up your home directory covers them.
Compose prefixes volume names with the project name, which is the directory you cloned into. The commands below assume the default postqueen-docker-compose_ prefix. Check docker volume ls and use the exact names you see.

Back up a volume

Stop her first. Postgres writes to postgres-volume the whole time she is up, and a tar of a live data directory can catch a half-written page.
Then archive each volume with a throwaway container that mounts the volume at /data and your current directory at /backup:
Run that line again for temporal-postgres-data and postqueen-uploads, changing the volume name and the archive name each time. Add postqueen-config to the list only if you put a file in it. Once the archives are on disk, bring her back:

Restore from that backup

Same trip, reversed. Replace the volume rather than untarring over a populated one, so nothing from the old contents survives underneath:
A Postgres data directory only opens under the major version that wrote it. The bundled Compose runs postgres:17-alpine for her database and postgres:16 for Temporal’s. Restore each archive into the image it came from.
A backup you have never restored is a guess. Try one on a spare machine before you need it for real.

If your media is in R2

Everything above assumes STORAGE_PROVIDER=local. Move media to Cloudflare R2 and postqueen-uploads stops filling up — along with it, a volume backup stops containing your media. The bucket becomes the thing to protect.
Back up the database and the bucket as one. Postgres stores only the URL of each media file, never the bytes. Restore a database from Monday next to a bucket from Friday and every post in between opens with broken media — nothing errors, the images are simply gone. Take them under the same timestamp and restore them together.
Copying a bucket is a bucket-to-bucket copy, which R2 performs server-side — nothing travels through your machine and there is no egress to pay for:
copy rather than sync. A file deleted from the live bucket should stay in the backup, which is the entire reason for having one.
Give that job its own R2 token, scoped to those two buckets: write where backups go, read the media. Reusing the application’s token hands your backup tool the ability to delete the media it exists to protect, and reusing an account-wide token hands it everything else you keep in R2. One thing R2 handles for you: an interrupted multipart upload leaves parts behind that appear in no listing but still occupy storage. Every bucket ships with a lifecycle rule that aborts them after seven days, under Settings → Object lifecycle rules.

Upgrading to a new image

The Compose file points at ghcr.io/gkhankinay/postqueen-app:latest. Two commands move you to a newer build:
pull fetches the new image, and up -d recreates the containers whose image changed. Nothing in your Compose file has to change. Back up postgres-volume before you pull. The next section is why.

Where migrations run

You never run a migration by hand. Her image starts pnpm run pm2, which clears the old pm2 processes, applies the Prisma schema, then starts backend, frontend and orchestrator. So the first thing a freshly pulled container does is reshape the database to match the schema that build expects, before anything is listening. Which command does the reshaping depends on one variable: The comparison is a literal string match against that short list, so it is stricter than it looks. no and off both read as true here and select prisma migrate deploy, and so does false with a trailing space. --accept-data-loss means she carries on instead of stopping to warn you, so a column the new schema dropped goes away with its contents. That is the whole argument for taking the backup first. The repository ships a single migration, 0_init, so the default db push path is the one nearly every install follows. Leave the variable alone unless you have a reason. Watch the first minute of the upgrade:

Pinning a version instead

Every release publishes twice, once under its version tag and once as latest. Pinning the version tag in your Compose file turns an upgrade into a deliberate edit rather than something docker compose pull decides for you:

The one flag that destroys everything

docker compose down -v deletes the volumes along with the containers. Her database and your uploads go in that one command, with no prompt and no undo, and the scheduled posts sitting in Temporal go with them. Plain docker compose down leaves every volume where it is. That is the one you want for a restart, a variable change or an upgrade.

Next Steps

Docker Compose

The install this page assumes, start to finish

Configure uploads

Move media to Cloudflare R2 and shrink what you back up

System requirements

Disk, RAM and the services she needs

Support

When a restore or an upgrade does not go your way