Documentation Menu

Configuration reference

21 min read

The CMS reads 54 environment variables, and this page gives each one's meaning, its default, when you need it and what breaks when it is wrong.

The CMS reads its configuration from the process environment and from nothing else: no file, no database setting, no dashboard screen. It validates its variables once, at boot, before it opens a port. When one is missing or malformed it refuses to start and names every offending variable in one message, so you fix a bad deployment in one pass rather than one restart per mistake:

ConfigError: Invalid configuration:
SESSION_SECRET: must be at least 32 characters
TRUST_PROXY: Invalid input: expected number, received NaN
WEBHOOK_SECRET_KEY: required: 32 bytes, base64 (openssl rand -base64 32)

The one exception is JSON_BODY_LIMIT, which is checked only for being non-empty; its row under Body and import limits says what a bad value does instead.

Five variables have no default and are always required: MONGO_URI, REDIS_URL, SESSION_SECRET, BASE_URL and WEBHOOK_SECRET_KEY. Everything else has a default that suits a single instance, and you set it only to change that.

A few spellings recur in the tables below:

  • Switches are spelled exactly as the table says, and nothing else is accepted: 0 or 1 for the webhook switches, on or off for the sign-in providers, true or false for ASSET_S3_FORCE_PATH_STYLE, open or closed for registration. yes is a boot failure, not a yes.
  • Sizes use the bytes spellings: 25mb, 512kb, 5gb, or a plain number of bytes. nginx's 25m is not one of them.
  • Windows are in milliseconds: 60000 is one minute.
  • Lists are comma-separated, with no spaces needed.

Some variables are read only when another one switches them on — the provider credentials under AUTH_GITHUB=on or AUTH_GOOGLE=on, the bucket settings under ASSET_BACKEND=s3. Under the other value they are ignored, not refused, so an environment can keep them filled in while the switch is off.

Server

Variable Meaning Default Needed when If wrong
PORT The port the process listens on. The dashboard, /api/v1, /d/v1, /healthz and /readyz are all served from it. 3000 (the image sets 3000 too) Only to listen elsewhere; the Helm chart's container always listens on 3000. Not a positive whole number: the CMS refuses to start. A port your proxy, port mapping or probes do not expect: nothing answers there, the image's own health check, which asks port 3000, marks the container unhealthy, and probes aimed at 3000 fail.
DASHBOARD_DIST The dashboard's build, which the API serves at / from the same origin, so the session cookie needs no CORS. A relative path is resolved against the API's own directory. ../dashboard/dist; the image sets /app/apps/dashboard/dist Never with the image. Only when you run the CMS from its source with the dashboard built somewhere else. A directory that does not exist is not an error: the CMS serves the APIs only, logs dashboard.disabled: no dashboard build at … once at boot, and content managers find no dashboard at /.
SHUTDOWN_GRACE_MS How long a shutdown waits for requests already in flight before closing anyway. It also caps how long a reader waits for an image variant another replica is making: the grace period less two seconds, at most 20 seconds. 10000 When the orchestrator's stop timeout is not comfortably longer, as Docker's own default of 10 seconds is not, or when you serve very large images whose variants take longer than 8 seconds to encode. Longer than the orchestrator's own grace period: the SIGKILL lands mid-drain and in-flight requests are cut off. Too short: slow requests are dropped at every deploy, and readers waiting for a large variant get a 503 sooner.

Data stores

Variable Meaning Default Needed when If wrong
MONGO_URI The Mongo connection string, mongodb:// or mongodb+srv://. It must address a replica set: the CMS writes a project's creation and a CLI push in one transaction, and transactions need one. A single-node set is enough. None Always. Missing or not a URL: the CMS refuses to start. A standalone mongod: every write that needs a transaction fails, creating a project and every quillet push among them. Unreachable: /readyz answers 503 with "mongo":"down" and requests fail.
REDIS_URL The Redis connection string. Redis holds sessions, rate-limit counters, the delivery cache and the image-transform locks. None Always. Missing or not a URL: the CMS refuses to start. Unreachable: /readyz answers 503 with "redis":"down", nobody can sign in, and every rate-limited request is refused with 503 rather than admitted uncounted.
REDIS_KEY_PREFIX What every Redis key the CMS writes begins with: sessions, counters, cache and locks together. q: Only to share one Redis between two installations that must not see each other. Two installations under the same prefix on one Redis behave as one: each clears the other's delivery cache and shares its sessions and rate limits. Changed on a running instance: everyone is signed out and the cache starts cold. Empty: the CMS refuses to start.

Sessions and URL

Variable Meaning Default Needed when If wrong
SESSION_SECRET Signs the session cookie and the short-lived cookie of a GitHub or Google sign-in. Unless ASSET_SIGNING_SECRET is set, the key that signs private files' links is derived from it too. Generate one with openssl rand -base64 48. None Always. Shorter than 32 characters or missing: the CMS refuses to start. Changed: every session ends and everyone signs in again; without ASSET_SIGNING_SECRET, every outstanding private-file link stops working too. A value copied from an example: anyone who has read the example knows your secret.
BASE_URL Where this deployment is reachable, as the browser sees it. The CMS builds absolute URLs from it: the sign-in callbacks, the asset URLs it hands out when ASSET_PUBLIC_URL is empty, and the address in its boot log line. An https:// value also marks the session cookie Secure. None Always. Missing or not a URL: the CMS refuses to start. A host other than the real one: GitHub and Google refuse the callback, and image and file URLs in delivered content point at the wrong host. https:// while you browse over plain http://: the browser drops the Secure cookie, so signing in appears to work and lands you back on the sign-in page.

Logging

Variable Meaning Default Needed when If wrong
LOG_LEVEL How much the log writes: debug, info, warn or error. The log is one JSON object per line on standard output. info When you are investigating something (debug) or want less noise (warn). Any other word: the CMS refuses to start. error: refused provider sign-ins and other warn lines vanish, so a misconfigured provider fails silently. debug in production: the framework's own boot chatter and much more fill the log.

Proxy and rate limits

Variable Meaning Default Needed when If wrong
TRUST_PROXY How many proxies sit in front of the CMS. It counts back this many hops through X-Forwarded-For to find the caller's address, which every per-address rate limit is counted against. A hop count, never true. 0; the Helm chart sets 1 Whenever an ingress, load balancer or reverse proxy forwards traffic to the CMS: 1 for one hop. 0 behind a proxy: every caller looks like the proxy and shares one budget, so one person's mistyped passwords lock everyone out of signing in. Higher than the real number of hops: a caller writes its own X-Forwarded-For and gets a fresh budget on every request. true: the CMS refuses to start.
AUTH_RATE_LIMIT Sign-in, registration and provider sign-in attempts allowed per address per window. Each of those routes counts its own budget. 10 To loosen it for a team behind one office address, or tighten it. Too low: people behind a shared address get 429 after a few attempts. Too high: password guessing gets more tries per minute. Zero, negative or not a number: the CMS refuses to start.
AUTH_RATE_LIMIT_WINDOW_MS The length of that window. 60000 When you change the window rather than the count. Too long: a locked-out person waits that long before trying again. Too short: the limit barely slows guessing. Not a positive number: the CMS refuses to start.

Registration and sign-in

Variable Meaning Default Needed when If wrong
AUTH_REGISTRATION Whether the password sign-up form is open or closed. Closed turns POST /api/v1/auth/register off for everyone and hides the form; existing accounts keep working, and a provider below still creates accounts for its members. open Set closed once the first accounts exist on any instance strangers can reach. open on a public instance: anyone who finds the URL can create an account and their own projects. closed before anyone has an account and with no provider on: nobody can get in. Any other word: the CMS refuses to start.
AUTH_GITHUB Sign in with GitHub, on or off. Under on the three variables below are required; under off they are ignored. off When people should sign in with GitHub. on without a client id, a secret or an organisation: the CMS refuses to start, naming each missing one. true or 1: the CMS refuses to start.
AUTH_GITHUB_CLIENT_ID The client id of the OAuth App you registered on GitHub with the callback <BASE_URL>/api/v1/auth/github/callback. Empty Under AUTH_GITHUB=on. Empty under on: the CMS refuses to start. An id that is not your app's: Continue with GitHub ends on an error page at GitHub instead of coming back.
AUTH_GITHUB_CLIENT_SECRET That OAuth App's client secret. A real one belongs in your secret store, not in a file. Empty Under AUTH_GITHUB=on. Empty under on: the CMS refuses to start. Wrong: GitHub refuses the code exchange, and the person lands back on the sign-in page with "GitHub sign-in did not complete. Try again in a moment."
AUTH_GITHUB_ALLOWED_ORGS Comma-separated GitHub organisation logins whose members may sign in, compared without regard to case. Membership is checked on every sign-in with the person's own token. Empty Under AUTH_GITHUB=on. Empty under on: the CMS refuses to start. A misspelt login, or an organisation that restricts third-party apps and has not approved yours: every member is refused with "Your GitHub account is not in an organisation that is allowed to sign in here."
AUTH_GOOGLE Sign in with Google, on or off, with the same rules as AUTH_GITHUB. off When people should sign in with Google. on without a client id, a secret or a domain: the CMS refuses to start, naming each missing one. Any spelling but on or off: the CMS refuses to start.
AUTH_GOOGLE_CLIENT_ID The id of the OAuth client you created in Google Cloud Console with the redirect URI <BASE_URL>/api/v1/auth/google/callback. Google accepts http:// there only for localhost. Empty Under AUTH_GOOGLE=on. Empty under on: the CMS refuses to start. Wrong, or the redirect URI not registered: Google shows its own error page and never sends the person back.
AUTH_GOOGLE_CLIENT_SECRET That OAuth client's secret, kept in your secret store. Empty Under AUTH_GOOGLE=on. Empty under on: the CMS refuses to start. Wrong: the exchange fails and the person lands back on the sign-in page with "Google sign-in did not complete. Try again in a moment."
AUTH_GOOGLE_ALLOWED_DOMAINS Comma-separated Google Workspace domains whose accounts may sign in, matched against the account's hosted-domain claim rather than its address. * admits any Google account with a verified address, personal ones included. Empty Under AUTH_GOOGLE=on. Empty under on: the CMS refuses to start. A domain that is not your Workspace's: everyone is refused with "Your Google account is not in a domain that is allowed to sign in here", and personal accounts always are. * on a public instance: anyone with a Google account can create an account, whatever AUTH_REGISTRATION says.

Delivery limits

Variable Meaning Default Needed when If wrong
DELIVERY_RATE_LIMIT Delivery reads allowed per key per window. Counted per key rather than per address, because every reader of a browser-side site shares one key. 600 When a site's traffic, or a build that reads every page, needs more than ten reads a second on one key. Too low: a busy site or a large static build gets 429 and pages fail to load or build. Too high: one leaked key can read as fast as it likes. Not a positive number: the CMS refuses to start.
DELIVERY_RATE_LIMIT_WINDOW_MS The length of that window. 60000 When you change the window rather than the count. Too long: a site that hit the limit stays refused for that long. Too short: bursts pass the limit freely. Not a positive number: the CMS refuses to start.
DELIVERY_PREAUTH_RATE_LIMIT Delivery requests one address may make per window before it is turned away, counted before the key is checked so a stream of invalid keys cannot keep buying expensive hash checks. Only as accurate as TRUST_PROXY. 1200 When one server legitimately sends more than this, such as a build machine holding several keys. Too low, or TRUST_PROXY left at 0 behind a proxy so every caller shares the proxy's address: legitimate readers are refused 429 together. Too high: invalid credentials cost the server CPU for longer. Not a positive number: the CMS refuses to start.
DELIVERY_PREAUTH_RATE_LIMIT_WINDOW_MS The length of that window. 60000 When you change the window rather than the count. Too long: an address that tripped the limit stays refused for that long. Too short: the guard against key guessing barely holds. Not a positive number: the CMS refuses to start.
KEY_VERIFY_CONCURRENCY How many API key verifications may be hashing at once in one process; the rest queue. The hash is expensive on purpose, and this keeps a burst from filling the pool that file reads and image transforms also use. 4 Rarely; only with more CPU per process and a measured queue. Too high: a burst of key checks, even invalid ones, leaves file reads and image transforms waiting behind it. Too low: requests with a key wait in line under load. Not a positive number: the CMS refuses to start.

Body and import limits

Variable Meaning Default Needed when If wrong
JSON_BODY_LIMIT The largest request body the API reads, in bytes spellings with a two-letter unit: 10mb, 512kb, or a plain number of bytes. One CLI push carries up to 500 documents, so this is measured in megabytes. Uploads are not counted here. Unlike every other size, it is not checked at boot beyond being non-empty. 10mb (two-letter unit) When a push of large documents comes back 413. A one-letter unit such as nginx's 10m is read as 10 bytes: the CMS starts, and then every save, push and sign-in larger than that is refused 413. A value that is not a size at all, such as ten, stops the CMS during startup with option limit "ten" is invalid, outside the usual one-message list. Too low: a push comes back 413 with payload_too_large, and the client has to split the batch. Too high: one request can make the process hold that much in memory. Any proxy in front with a smaller limit cuts the request before the CMS sees it.
IMPORT_MAX_BYTES The same ceiling for POST …/import alone, measured after the CLI's gzip is decoded. An archive's metadata is every version of every document, which is far more than a push. 256mb When restoring a project whose archive metadata is larger than that. Too low: restoring a real project fails with 413 before any file is uploaded. Not a size: the CMS refuses to start ("use a byte size such as 25mb"). A proxy in front needs the larger limit on that path, or the restore is cut at the edge.

Assets

Variable Meaning Default Needed when If wrong
ASSET_BACKEND Where uploaded files' bytes live: disk, a mounted directory, or s3, any S3-compatible bucket. The store is checked once at boot, before the port opens. disk Set s3 for a bucket, or when more than one replica runs without a shared volume. Any other word: the CMS refuses to start. A store that fails its boot check: the CMS refuses to start and says why. A store that goes away later: the asset routes answer 503 with Retry-After and everything else keeps working.
ASSET_DISK_ROOT The directory disk writes to. A relative path is resolved against the API's own directory. data/assets Always with disk in a container: set it to where your volume is mounted, such as /data/assets. Left at the default in the image: the CMS writes files into the container's own filesystem, and every file is lost when the container is replaced; under a read-only root filesystem it refuses to start instead. Not writable: the boot check fails and the CMS refuses to start. Two replicas on two separate volumes: each answers 404 for the other's files.
ASSET_S3_ENDPOINT The bucket's endpoint, such as https://fra1.digitaloceanspaces.com. None Under ASSET_BACKEND=s3. Missing under s3: the CMS refuses to start. Not a URL: the CMS refuses to start, even under disk. The wrong host: the boot check cannot reach the bucket and the CMS refuses to start.
ASSET_S3_REGION The region. DigitalOcean Spaces and MinIO ignore it; the S3 client requires one. us-east-1 Under s3, when your store checks the region. A store that checks it refuses the requests: the boot check fails and the CMS refuses to start. Empty: the CMS refuses to start.
ASSET_S3_BUCKET The bucket files are written to. Keep it private: the CMS is its only reader. None Under s3. Missing under s3: the CMS refuses to start. A bucket that does not exist or that these credentials cannot see: the boot check fails and the CMS refuses to start. A public bucket: anyone who can list it reads every file, private ones included, without a signed link.
ASSET_S3_ACCESS_KEY_ID The access key for the bucket. A real one belongs in your secret store. None Under s3. Missing under s3: the CMS refuses to start. Rejected by the store: the boot check fails and the CMS refuses to start. A key that can see the bucket but not write to it: the CMS starts and every upload fails.
ASSET_S3_SECRET_ACCESS_KEY The secret key that goes with it, kept in the same secret store. None Under s3. Missing under s3: the CMS refuses to start. Wrong: the store rejects the signature, the boot check fails, and the CMS refuses to start.
ASSET_S3_FORCE_PATH_STYLE Path-style addressing (endpoint/bucket/key) instead of virtual-hosted (bucket.endpoint/key): true for MinIO and most self-hosted stores, false for DigitalOcean Spaces. false Under s3, set to what your store expects. The wrong style for your store: the bucket's address is wrong, the boot check fails, and the CMS refuses to start. 1 or yes: the CMS refuses to start.
ASSET_S3_PREFIX A key prefix inside the bucket, for a bucket shared with something else. Letters, digits, ., _, - and /, no leading slash, no empty, . or .. segment. Empty Under s3, when the bucket holds other things. A leading slash or a .. segment: the CMS refuses to start. Changed after files were uploaded: the CMS looks for them under the new prefix and every existing file answers 404.
ASSET_PUBLIC_URL A CDN origin in front of /d/v1/*/assets/*. When set, every asset URL the CMS hands out is built on it instead of on BASE_URL. The CDN's origin is the CMS, never the bucket. No trailing slash. Empty, which means BASE_URL When a CDN serves your images and files. A trailing slash: the CMS refuses to start. A development value such as http://localhost:3000 left in production: every image and file URL in delivered content points at localhost and breaks on every reader's machine. A CDN in front of the bucket instead of the CMS: variants, SVG safety headers and private files stop working.
ASSET_SIGNING_SECRET Signs the links that open a private file. Blank, the key is derived from SESSION_SECRET. Setting it lets you rotate file links without signing anyone out. Unset, derived from SESSION_SECRET When you want to rotate private links on their own, or never tie them to the session secret. Set but shorter than 32 characters: the CMS refuses to start. Changed: every outstanding private-file link stops working at once. A value copied from an example: anyone who has read the example can sign links to your private files.
ASSET_MAX_BYTES The largest single upload, in bytes spellings. 25mb When people upload larger video or PDF files. An upload over it is refused 413. Larger than what your proxy or ingress accepts: the upload is cut at the edge and the person sees the proxy's error, not the CMS's. Not a size: the CMS refuses to start.
ASSET_PROJECT_QUOTA_BYTES How much one project may hold, in the same spellings. 0 is unlimited. 5gb When a project needs more room, or you want to cap it lower. Too low: uploads are refused 409 with "delete something first" while the project still has work to add. 0 on a shared instance: one project can fill the disk or the bucket for everyone. Not a size: the CMS refuses to start.
ASSET_MIME_ALLOWLIST The media types an upload may be recognised as, comma-separated. The type is read from the file's bytes; what the browser declared and the file's extension are ignored. JPEG, PNG, GIF, WebP, AVIF, SVG, PDF, MP4, WebM, MP3 and WOFF2 (image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,application/pdf,video/mp4,video/webm,audio/mpeg,font/woff2) When a project needs a type outside that list. A type left off: those uploads are refused with "Files of type … are not allowed." image/heic added: HEIC uploads are still refused with "This image could not be read", because the image library cannot decode it. A malformed entry or an empty list: the CMS refuses to start.
ASSET_IMAGE_WIDTHS The w= widths a scaled image may be asked for, comma-separated. Anything else is refused, which keeps a caller from filling the cache with one variant per pixel. 320,640,960,1280,1920,2560 When your site's layout needs other widths. A width removed while sites still ask for it: those srcset entries answer 422 and the images break. Anything that is not a list of positive whole numbers: the CMS refuses to start.
ASSET_UPLOAD_RATE_LIMIT Uploads allowed per signed-in person or per key per window. 60 When people or scripts upload larger batches at once. Too low: a large batch dropped into Media stops part-way with 429 and a Retry-After. Too high: one key can fill the quota in a burst. Not a positive number: the CMS refuses to start.
ASSET_UPLOAD_RATE_LIMIT_WINDOW_MS The length of that window. 60000 When you change the window rather than the count. Too long: someone who hit the limit waits that long to finish a batch. Too short: the limit barely slows a runaway script. Not a positive number: the CMS refuses to start.
ASSET_TRANSFORM_CONCURRENCY How many images one process scales and re-encodes at once. A memory ceiling: one transform of a 50-megapixel image needs about 200 MB, and past this number a variant request gets 503 with Retry-After. 4 Only together with the container's memory limit. Raised without more memory: a burst of new variants makes the process run out of memory and the container is killed mid-request. Too low: under load, readers get 503 for images that are not cached yet. Not a positive number: the CMS refuses to start.

Webhooks

Variable Meaning Default Needed when If wrong
WEBHOOK_SECRET_KEY Encrypts every webhook's signing secret at rest. Exactly 32 bytes in standard base64; generate it with openssl rand -base64 32. None Always: the CMS refuses to start without it. Missing, not base64, or not 32 bytes: the CMS refuses to start. Lost or changed: the CMS can no longer sign deliveries for existing webhooks, and each one has to have its secret rotated. The example key of 32 zero bytes (AAAA…A=) boots, and anyone can read it.
WEBHOOK_ALLOW_HTTP 1 accepts http:// webhook URLs, for local development; 0 allows https:// only. 0 Only on a development machine whose receiver has no TLS. 0 in development: a webhook at http://localhost is refused when you save it. 1 in production: deliveries and their signatures cross the network unencrypted. true: the CMS refuses to start.
WEBHOOK_ALLOW_PRIVATE_NETWORKS 1 lets a webhook reach private, loopback and link-local addresses, for a receiver inside the same cluster or machine. With 0, such a URL, or a hostname that resolves to one, is refused when it is saved and again when it is called. 0 When the receiver lives on a private network next to the CMS. 0 with an in-cluster receiver: the webhook is refused when saved. 1 on an instance where strangers can create projects: anyone with a project can make the CMS send requests to your internal services. true: the CMS refuses to start.
WEBHOOK_TIMEOUT_MS How long one delivery attempt waits for the receiver's whole response. 10000 When a receiver is known to answer slowly. Too short: a slow but healthy receiver is counted as failing, retried, and paused after twenty failed attempts in a row. Too long: a dead receiver holds one of the eight delivery slots that much longer per attempt. Not a positive number: the CMS refuses to start.
WEBHOOK_RATE_LIMIT Deliveries per minute per webhook; the rest wait for the next minute. 60 When a busy project publishes faster than that and its receiver can keep up. Too low: deliveries back up, and sites hear about a publish minutes late. Too high: a burst of publishes can overwhelm a small receiver. Not a positive number: the CMS refuses to start.
WEBHOOK_WORKER 1 lets this process run the delivery loop. Every replica runs it, and a Redis lease picks one to send; 0 is for a process that should never send. 1 Set 0 only on processes where a separate worker sends deliveries. 0 on every process: no webhook is ever delivered, and deliveries wait in the queue. true: the CMS refuses to start.

Promotion

Variable Meaning Default Needed when If wrong
PROMOTION_RATE_LIMIT Compares, promotions and clones one person or key may ask for per window: one budget across the three. A hundred lets the dashboard read a 20,000-document report and promote it in one sitting. 100 When environments are larger than 20,000 documents, or a script promotes often. Too low: a large compare or promotion stops part-way with 429, and Try again starts the whole report again. Too high: a script can turn promotion into a write loop against the database. Not a positive number: the CMS refuses to start.
PROMOTION_RATE_LIMIT_WINDOW_MS The window that budget is spent in. 60000 When you change the window rather than the count. Too long: someone who used the budget waits that long before the next promotion. Too short: the limit no longer stops a runaway script. Not a positive number: the CMS refuses to start.

Last updated