NEW: the postqueen MCP server is live. Connect it in a minute
All posts

Self-Hosting postqueen with Docker: Setup, Backups, and Updates

postqueen team7 min readGuides

A social scheduler quietly accumulates two things that are painful to replace: the OAuth tokens that connect your accounts, and the archive of everything you have queued, published and measured. When both live on infrastructure you do not control, your publishing history is a vendor relationship rather than a directory you can copy.

postqueen is AGPL-3.0 licensed and ships a Docker image, so running the whole stack yourself is a supported path rather than a workaround. This guide covers the parts that decide whether it goes well: what the server actually needs, the published Compose file and what it starts, the environment variables worth understanding on day one, backups that restore, and an update routine that does not eat your queue.

Why Self-Host, and Why Not

The good reasons are narrow and specific. You keep the database, which means your post archive and your analytics are a pg_dump away from being portable. The OAuth tokens sit on your disk, under your key management. Your channel count is bounded by what each network's API allows and by your own patience, not by a plan tier. If your organization has rules about which region holds anything customer adjacent, a server in that region answers the question directly. And because the license is AGPL-3.0, the code that touches your accounts is code you can read, audit and patch.

The reasons not to self-host are just as concrete, and they collapse into one: you become the operator. You own Postgres upgrades, TLS renewal, disk space, and a developer app registration on every network you connect. Most of that is boring and fine. The part that bites is the failure mode. A web app that breaks shows you an error page within minutes, because someone tries to load it. A scheduling worker that dies breaks nothing you can see.

A scheduler that goes down never throws an error at you. It quietly stops publishing, and you find out from an empty calendar a week later.

So the real prerequisite is not RAM. It is one person who will notice that nothing went out on Tuesday.

What the Server Actually Needs

Nothing exotic. A small VPS handles a single team comfortably, and the load that matters comes from workers pushing media to networks rather than from anyone using the dashboard.

  • A Linux host with Docker Engine and the Compose plugin. Two vCPUs and 4 GB of RAM is the floor rather than a comfortable starting point, because the Temporal stack below brings an Elasticsearch with it; 8 GB is the number to ask for. Video uploads are the thing most likely to make you want more still.
  • PostgreSQL for the durable data: accounts, channels, scheduled posts, analytics. Run it in Compose or point at a managed instance you already trust.
  • Redis for the job queue that fires posts at their scheduled time. This is not an optional cache. Without it, nothing publishes.
  • Temporal, which the app treats as a hard dependency: it will not start until Temporal reports healthy. Temporal in turn wants its own Postgres and an Elasticsearch. The published Compose file wires all of it up for you, and it is the reason the stack is bigger than it looks.
  • A real domain with real TLS. Social networks will not send an OAuth callback to localhost, and they will not accept a self-signed certificate.
  • Storage for media, either a mounted volume you remember to back up or an S3-compatible bucket such as MinIO.
  • A developer app on each network you plan to connect, with your own callback URL registered on it.

That last item is the one that surprises people, so plan for it first. On a hosted instance the OAuth apps already exist and have already been reviewed. Self-hosting means you register your own app per network and keep it in good standing. Some networks hand you credentials in five minutes. Others want a use case description, a privacy policy URL and a review that takes days, and a few keep the interesting endpoints behind approval. Budget attention for this before you budget for the server, because it is the step that sets your real launch date.

The Compose File, and What Actually Runs

There is a published Compose file, and it is the one to start from rather than a stack you assemble by hand. The thing worth knowing before the first boot is that the app does not run alone. It waits for Postgres, Redis and a Temporal server to report healthy before it will start, and Temporal brings its own Postgres and Elasticsearch with it. That is a nine service stack, not three, which is why a minimal file copied from somewhere else leaves you watching a container that never comes up.

terminal
git clone https://github.com/GkhanKINAY/postqueen-docker-compose
cd postqueen-docker-compose
 
# Edit docker-compose.yaml before the first boot. Everything you have to
# change sits in the required block at the top of the postqueen service:
# MAIN_URL, FRONTEND_URL, NEXT_PUBLIC_BACKEND_URL your real https domain
# JWT_SECRET a long random string
# DATABASE_URL, REDIS_URL passwords that are not the defaults
 
docker compose up -d
docker compose logs -f postqueen

Two things are worth knowing about the file as published. It maps the app to host port 4007, and it maps it on every interface, so on a public server put a reverse proxy such as Caddy, nginx or Traefik in front, terminate TLS there, and narrow the mapping to 127.0.0.1:4007:5000 so the proxy is the only thing facing the internet. And every dependency carries a healthcheck, which is what stops the app racing Postgres and Temporal on a cold boot and filling the logs with connection errors while you are trying to read them.

The Environment Variables That Matter First

The image reads a longer list than the required block you just edited, but a handful decide whether the instance behaves.

  • The public URLs: what a browser and a social network see. They have to match your real domain exactly, https included. Nearly every OAuth callback failure on a fresh instance is one of these being subtly wrong.
  • DATABASE_URL and REDIS_URL: connection strings. If you move Postgres to a managed instance later, this is the only line that changes.
  • TEMPORAL_ADDRESS: where the Temporal server answers, temporal:7233 inside the Compose network. Point it elsewhere only if you run Temporal outside this stack.
  • JWT_SECRET: a long random string you generate once. Treat it as a credential, back it up, and know that rotating it signs everyone out.
  • Storage settings: keep the local volume if you like simple, or switch to S3-compatible storage once media gets big enough that backing up a volume stops being fun.
  • Registration control: after your team has accounts, close signups. An open instance on a public domain is an invitation, and what it invites people to is your OAuth tokens.

Each network you enable then adds its own client id and secret from the developer app you registered earlier. The repository's environment reference lists the full set. Work through it once, put the result in a file that never enters git, and mount it with env_file rather than inlining secrets in the Compose file the way the example does for brevity.

Backups That Actually Restore

There are exactly two things worth backing up: the Postgres database and the uploads directory. The database holds accounts, channel connections, the queue and analytics. Uploads hold the media those posts point at. Everything else is a container you can pull again in thirty seconds.

terminal
# Nightly dump, written somewhere that is not this host. The service,
# user and database names are the ones in the published Compose file.
docker compose exec -T postqueen-postgres \
pg_dump -U postqueen-user -Fc postqueen-db-local > pq-$(date +%F).dump
 
# The media volume. Compose prefixes volume names with the project
# directory, so ask docker for the real name instead of guessing it.
docker volume ls --filter name=postqueen-uploads
docker run --rm -v postqueen-docker-compose_postqueen-uploads:/data \
-v "$PWD":/backup alpine \
tar czf /backup/uploads-$(date +%F).tar.gz -C /data .
 
# Prove the dump works: restore it into a scratch database
docker compose exec -T postqueen-postgres \
createdb -U postqueen-user restore_test
docker compose exec -T postqueen-postgres \
pg_restore -U postqueen-user -d restore_test < pq-2026-07-14.dump

The third command is the one everybody skips, and it is the only one that matters. A backup you have never restored is a hypothesis. Run it into a scratch database once a quarter and the exercise costs ten minutes; discover the problem during an incident and it costs your weekend. Back up the environment file alongside the dumps as well, somewhere at least as protected. The dump is half an instance. The configuration it ran with is the other half.

Updating Without Losing the Queue

Pin a version tag in production and let the latest tag be something you point a staging box at. The tag is not the safety mechanism, though. The dump is. Migrations run on boot and they run forward, which means rolling back is not a matter of re-tagging the image: it is restoring the dump you took beforehand. So take one beforehand, every time. It costs fifteen seconds and it turns a bad upgrade from an incident into an inconvenience.

terminal
# Always dump before an upgrade. This is your rollback plan.
docker compose exec -T postqueen-postgres \
pg_dump -U postqueen-user -Fc postqueen-db-local > pre-upgrade.dump
 
# Then pull and restart. Migrations run on boot.
docker compose pull
docker compose up -d
docker compose logs -f postqueen

Afterwards, watch the workers rather than the homepage. A dashboard that loads proves the web process is alive and proves nothing at all about the queue. Schedule a throwaway post to a low-stakes channel five minutes out and confirm it actually fires. That is the only smoke test that covers the part of the system that breaks silently.

When the Hosted Version Is the Right Call

Self-hosting is free in license terms and it is not free in attention. If nobody on the team wants to own Postgres dumps, certificate renewal and a developer app review queue, the postqueen cloud plans exist for exactly that reason, starting at $20 a month with a 7-day trial. It is the same codebase either way: same 30 channels, same CLI, same REST API, same MCP endpoint. Moving in either direction later is a configuration change rather than a change of habits.

So decide on the honest question, which is not a technical one. Someone has to notice when Tuesday is empty. If that person exists and wants the control, self-host, and work the checklist in order: OAuth apps first, Compose second, backups before you connect anything real. If that person does not exist, pay for the queue and spend the attention on the posts instead.

Ready to get started?

Queue this week's posts in minutes, or hand the whole thing to your favorite AI. postqueen keeps publishing either way.

Seven days free. Cancel in one click from settings.