Skip to main content
She reads everything she needs from environment variables. Change one and restart her before you expect it to take effect. Under Docker Compose that means 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.
Start from the example env file in the app repository, which is the file to copy on a fresh install. This page then groups the variables by purpose and explains what each one does. Neither list is a strict superset of the other, so if you meet a variable in one and not the other, it is real either way.
Variables marked Required are checked on boot, but only warned about. The check runs after the server is already listening and every problem it finds is logged as a Configuration issue line. Nothing throws and nothing exits.So a running container is not proof that the configuration is right. Read the first minute of docker compose logs on a fresh deployment and look for that phrase.

Required core

The boot check treats eight variables as non-optional: the six below, plus MAIN_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.
Read 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:
Separate hostnames, which applies when you run the frontend and backend as separate services, from source or in Kubernetes:
Dropping the /api suffix on a Compose install is a common mistake. Requests then land on the frontend instead of the backend and everything after sign-in fails. If you are unsure which shape you have, check the shipped docker-compose.yaml: it uses the /api form.
Note that a trailing slash on any of these URLs is reported as a configuration issue on boot.

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

Writing false turns most of these on. Only five variables compare against the literal string true: DISABLE_REGISTRATION, PASSWORDLESS_LOGIN, DISABLE_SSRF_PROTECTION, TEMPORAL_TLS and EMAIL_SECURE. Every other switch on this page is read as !!process.env.NAME, and in JavaScript the string "false" is truthy.So for IS_GENERAL, POSTQUEEN_GENERIC_OAUTH, DISABLE_IMAGE_COMPRESSION, RUN_CRON and their neighbours: set the variable to enable, and remove the line to disable. Setting it to false, 0 or no enables it just as surely as true does.

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

Dev only. Never set in production: it disables security checks that exist for a reason.

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.

Email

See also: Email configuration.
EMAIL_PROVIDER decides more than email. It also decides whether a new email-and-password account has to be activated before it can be used: with a provider named, the account is created inactive and waits for the activation link, and with none named it is active immediately.The trap is naming a provider you have not finished configuring. EMAIL_PROVIDER=resend with no RESEND_API_KEY counts as a provider, so every new signup is created inactive, the activation mail fails quietly, and nobody can get in. Either configure the provider fully or leave EMAIL_PROVIDER unset. The key on its own changes nothing.

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


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. The GUARD_* 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
Moltbook needs no environment variable either. It has a connect screen of its own that registers the channel and issues its key for you. Every network in Providers connects from Add Channel whether or not it has a row in this table.
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.
LISTMONK_* does not configure the Listmonk channel. The channel you connect in Add Channel to publish campaigns asks for its URL, username and password in the connect form, and it ignores these variables completely. Setting them will not make the channel appear, and leaving them unset will not stop it working. See Listmonk for the channel. There is no Beehiiv channel at all: Beehiiv exists only as a sender for the list above.

Runtime & build

These are read from the environment but are typically set by your runtime, hosting platform, or framework rather than set by hand.