docker compose down followed by up, because a plain restart reuses the container and
its old environment.
NEXT_PUBLIC_ variables do not need a rebuild here. That is the usual rule for a
Next.js app, and it does not apply to this image. Her layouts are server rendered on every
request and hand these values to the browser at runtime, so setting one in Compose and
restarting is enough. The single genuine exception is NEXT_PUBLIC_VERSION, which is baked
in when the image is built.Required core
The boot check treats eight variables as non-optional: the six below, plusMAIN_URL and STORAGE_PROVIDER, which are listed
under their own sections further down.
DATABASE_URL Required
PostgreSQL connection string used by Prisma.
REDIS_URL Required
Redis connection string used for queues, rate limiting, and short-lived caches.
JWT_SECRET Required
A long random string used to sign session JWTs.
It has a second job that decides how dangerous rotating it is. When ENCRYPTION_KEY is unset, this same value also encrypts the secrets PostQueen holds for you: your API keys, the OAuth client secrets and codes she issues, saved provider credentials, and cookie-based logins such as Skool. Change it in that state and those rows can no longer be decrypted, so the connections that depend on them break and re-entering each one by hand is the only way back.
What this does not cover. A channel’s own access and refresh tokens, the ones a social network hands back at the end of its OAuth flow, are stored as the network returned them. They are not encrypted with this key, so rotating it does not invalidate them, and the database is the security boundary for those. Treat a database dump accordingly. Compliance says the same thing from the other direction.
ENCRYPTION_KEY before you rotate this. Setting it is what makes rotating this variable safe, and on a fresh install it costs you nothing to do now.
ENCRYPTION_KEY
The key used to encrypt stored secrets: API keys, the OAuth client secrets and codes PostQueen issues, saved provider credentials and cookie logins. Unset, it falls back to JWT_SECRET, which is why every install that has never set it has the two jobs welded together. It does not cover a channel’s own access and refresh tokens, as described above.
Set it once, to the value JWT_SECRET currently holds. Nothing re-encrypts and nothing breaks, because the fallback was already using that value. What changes is that JWT_SECRET becomes a signing key you can rotate whenever you like, and the worst it can then do is log everyone out.
PREVIOUS_ENCRYPTION_KEY
The old value, while you are rotating ENCRYPTION_KEY. Decryption tries the current key first and falls back to this one, so rows written under the old key keep opening.
Rotate in this order: copy the current key into PREVIOUS_ENCRYPTION_KEY, put the new key in ENCRYPTION_KEY, restart. Leave both set afterwards. A row is only rewritten under the new key when something happens to rewrite it, so a refresh token for a channel nobody touches stays on the old key indefinitely, and dropping PREVIOUS_ENCRYPTION_KEY too early is what loses it.
FRONTEND_URL Required
The URL the browser uses to reach the PostQueen frontend. Used as the OAuth redirect base and for email links.
NEXT_PUBLIC_BACKEND_URL Required
The URL the browser uses to reach the PostQueen backend. What goes here depends on how you
deployed her, and the two shapes are not interchangeable.
One address, which is what the official image and Compose file do. A proxy inside the
container sends anything under /api to the backend, so the backend is a path on the same
origin:
BACKEND_INTERNAL_URL Required
The URL the frontend server uses to reach the backend, server to server. This is not a
public address. On the official image both run in the same container, so it stays
http://localhost:3000 no matter what your domain is. In a split deployment it is the
backend’s internal address, such as http://backend:3000.
Application behavior
DISABLE_REGISTRATION
Set to true and only the first account can be created. After that, sign-up is refused. Useful for self-host where you want full control.
It gates account creation only, never signing in. Everyone who already has an account keeps logging in normally, because the check is reached only on the branch that would create a new user.
One exception is worth knowing before you rely on this. Your own OIDC provider is exempt: a person who signs in through it for the first time still gets an account, even with this set. GitHub and Google are not exempt. So if your goal is to close the door completely, close it at your identity provider as well.
API_LIMIT
Per-hour limit on the public-API create-post endpoint. Defaults to 90. Channel and post quotas are tiered separately by plan.
RUN_CRON
When set, the backend process runs the scheduled-task workers. Leave unset on API-only instances when workers are deployed separately.
RESTRICT_UPLOAD_DOMAINS
A single substring that every media URL in a POST /public/v1/posts body must contain, usually your own host or CDN. Unset, any URL passes.
Two things about it surprise people. It is one value, not a list: the check is a plain substring test against the whole URL, so a.com,b.com demands that literal text and nothing ever matches. And it does not guard /public/v1/upload-from-url, which has its own rules: the URL must end in one of six extensions and must not point at a private address.
DISALLOW_PLUS
When set, any email address containing a + is refused at the door. It exists to stop one person opening account after account with plus-addressed aliases.
Read that carefully before you switch it on, because it applies to signing in as well as signing up. Anyone who already has an account at an address like you+postqueen@example.com is locked out of it the moment this is set. Only email and password are affected: sign-in through GitHub, Google or your own OIDC provider goes around the check entirely, and nothing in the interface changes.
IS_GENERAL
Switches the frontend between routes available to the open-source build (/launches) and the hosted build (/analytics). Set to "true" on self-host.
DISABLE_IMAGE_COMPRESSION
When truthy, the frontend skips client-side image compression on upload. Set this if you need pixel-exact originals at the cost of larger uploads.
NOT_SECURED
MAIN_URL
Primary application URL used for absolute links in some emails and SEO metadata. Falls back to FRONTEND_URL when not set.
EXTENSION_ID
The Chrome Extension ID for cookie-based platform integrations (e.g. Skool). Setup: Chrome Extension guide.
MOBILE_APP_SCHEME
URL scheme used for deep-linking from emails into the mobile app.
Storage
See also: Cloudflare R2 and Uploads & Storage.OAuth sign-in (OIDC)
See also: OAuth configuration.Temporal (workflow orchestration)
PostQueen uses Temporal for scheduled posts and background workflows. Self-host deployments need to run a Temporal stack (the official docker-compose ships with one). The scheduling architecture: How PostQueen works.Public API & MCP
AI / generation
Short-link providers
Pick one set. If more than one is configured the first in this order wins silently, and the choice is made once at startup. Setup walkthrough: Short Links.Dub.co
Short.io
Kutt.it
LinkDrip
Payments
Payments are only relevant if you are running PostQueen as a commercial service. A private
self-hosted install needs none of this, and leaving it unset simply means no billing.
Abuse protection
Rate limiting on the sign-in and sign-up endpoints is on by default and needs no configuration. TheGUARD_* family tunes it, TURNSTILE_SITE_KEY and TURNSTILE_SECRET add a captcha, and
PASSWORDLESS_LOGIN switches sign-in to emailed one-time codes. All of them, with their
defaults, are on Abuse Protection and Rate Limits.
Deployment identity
Cosmetic links and addresses she shows in the interface. Every one is optional.AFFILIATE_URL points at your programme, not ours. An Affiliate entry appears in the left
rail only when you set it, and it sends people to whatever address you give it. It also needs
billing switched on, so an install with no Stripe keys never shows it.There is no PostQueen-run programme behind this. The rail entry used to point at the vendor’s, which
sent every self-hosted install’s users somewhere their operator never chose, and that was removed.Analytics & error tracking
All of this is optional, and a self-hosted install runs perfectly well with none of it set. Set the keys for the service you already run and leave the rest alone.
All of these are read at runtime, so setting one and recreating the container is enough. The
build-time exceptions are the three Sentry source map variables, which only matter when you
build the image yourself.
Misc frontend
Social provider keys
You came here to find out which variable belongs to which network. This table is that lookup, and the setup page in the third column is where the key-by-key walkthrough lives. Not every network appears below, and that is the point. Eight of them ask for no environment variable at all, because they take their credentials in the connect form itself. You type the key once in Add Channel and never open your.env file
for them:
- Bluesky
- Dev.to
- Hashnode
- Lemmy
- Listmonk
- Medium
- Nostr
- WordPress
Two variable names break the pattern and cost people an afternoon. Kick signs its
secret as
KICK_SECRET, not KICK_CLIENT_SECRET. Dribbble spells its variables
with three b characters, DRIBBBLE_CLIENT_ID and DRIBBBLE_CLIENT_SECRET, while
the network itself is spelled with two.Product newsletter sender
This block trips people up, so read it before you copy anything into your.env
file. These variables have nothing to do with posting. They subscribe each new
PostQueen signup to a mailing list you own, and that is all they do.
When someone registers, PostQueen calls one newsletter sender with their email
address. It picks Beehiiv if BEEHIIVE_API_KEY is set, otherwise Listmonk if
LISTMONK_API_KEY is set, otherwise nobody. Leave all of them unset and no
signup email is ever forwarded anywhere.