Configuration reference#
Every environment variable the services, scripts and compose files read, by area. The file that holds them is one
env file per stack: .env in development, .env.<slug> for a workspace (written by scripts/provision-workspace.sh,
mode 600). Docker Compose passes it to the containers (env_file); shell-exported variables win over the file. The
commented template with explanations is .env.example.
Conventions: Secret = never log, never commit, back up with the database (secrets and keys);
Restart = needs the service restarted (all of them are read at start unless a row says "read per request");
unset = the feature is off or the built-in default applies. The release gate scripts/release/check-docs.py fails
when a variable read by the services or listed in .env.example is missing from this page.
1. Identity of the installation#
| Variable |
Used by |
Default |
Meaning |
WHOOBE_HOSTNAME |
core-api, asset-processor, gateway (CORS), Vite builds |
localhost |
host name all derived URLs are built from; credentialed CORS is allowed for it (any port or scheme) |
DB_HOSTNAME, S3_ENDPOINT_HOSTNAME |
.env.example interpolation |
localhost |
convenience names for DB_HOST and S3_ENDPOINT |
NODE_ENV |
all backends |
production in the images |
production hides SQL and stack text in 5xx bodies, disables loopback outbound by default, stops allowing localhost origins for credentialed CORS |
PORT |
asset-processor, core-api |
3002 / 3003 |
listening port inside the container (do not change in Compose) |
GATEWAY_PORT |
gateway |
3001 |
same, for the gateway |
CORS_ALLOWED_HOSTS, CORS_ALLOWED_ORIGINS |
all backends |
unset |
extra hosts / exact origins allowed to send credentialed requests |
ADMIN_PUBLIC_URL |
core-api |
http://$WHOOBE_HOSTNAME:3000 |
link in password-reset e-mails |
TRUST_PROXY |
all backends |
unset |
reverse-proxy hops (1), loopback or CIDR list, so that req.ip is the client; required behind nginx, otherwise limiters and the login lockout see the proxy |
COOKIE_SECURE |
core-api |
automatic (HTTPS or X-Forwarded-Proto: https) |
true forces the Secure flag on the session cookie |
2. Database (PostgreSQL)#
| Variable |
Used by |
Default |
Secret |
Meaning |
POSTGRES_USER, POSTGRES_DB |
the postgres container, Compose |
postgres, whoobe_db |
|
database server bootstrap |
POSTGRES_PASSWORD |
the postgres container |
none (Compose refuses to start) |
yes |
|
DB_HOST, DB_PORT, DB_USER, DB_NAME |
core-api, asset-processor, gateway, migration-tool |
localhost, 5432, postgres, whoobe_db |
|
connection |
DB_PASSWORD |
same |
none: the service stops at start if it is unset, unless NODE_ENV is development or test (F-057); Compose requires it |
yes |
|
DB_SSL, DB_SSL_CA |
same |
disable |
|
TLS to PostgreSQL: disable | require | verify-full; CA as path or PEM (encryption) |
WS_POSTGRES_PORT, WS_DATA_BIND_ADDRESS |
Compose |
5432, 127.0.0.1 |
|
host side of the database port; set 0.0.0.0 only with a firewall |
3. Object storage (S3 API)#
| Variable |
Used by |
Default |
Secret |
Meaning |
COMPOSE_PROFILES |
Compose |
local-s3 |
|
local-s3 starts Garage; remove it to use an external S3 |
S3_ENDPOINT |
host processes |
http://$S3_ENDPOINT_HOSTNAME:9000 |
|
endpoint for processes outside Docker |
S3_CONTAINER_ENDPOINT |
Compose services |
http://garage:3900 |
|
endpoint the containers call (external S3: its URL) |
S3_BUCKET, S3_REGION |
asset-processor, core-api |
whoobe-cms, us-east-1 |
|
|
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
asset-processor, core-api, Garage |
none |
yes |
Garage needs GK + 24 hex and 64 hex |
S3_FORCE_PATH_STYLE |
asset-processor |
true |
|
false for AWS S3 |
S3_KEY_PREFIX |
asset-processor |
empty |
|
prefix inside the bucket |
S3_PUBLIC_BASE_URL |
asset-processor |
http://$WHOOBE_HOSTNAME:3002/dam-asset |
|
base of the public asset URLs the API returns |
WS_S3_API_PORT |
Compose |
9000 |
|
host side of the S3 port |
MINIO_ENDPOINT, MINIO_BUCKET, MINIO_ROOT_USER, MINIO_ROOT_PASSWORD |
legacy aliases of S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY still read by core-api and asset-processor |
|
user and password are secret |
use the S3_* names; MinIO itself is no longer part of the stack (storage backends) |
ASSET_PROCESSOR_URL |
core-api, logger |
http://$WHOOBE_HOSTNAME:3002 |
|
where core-api reaches asset-processor (inside Compose: the service name) |
DAM_SERVICE_TOKEN |
core-api, asset-processor, logger |
none |
yes |
the service-to-service bearer (auth) |
DAM_ENFORCE_ASSET_VISIBILITY |
asset-processor |
true |
|
false: anyone with a URL can read any asset |
ASSET_MAX_FILE_BYTES, ASSET_MAX_COUNT |
asset-processor |
50 MB, no count limit |
|
upload caps |
ASSET_MAX_INPUT_PIXELS |
asset-processor, core |
100000000 |
|
largest image decoded; 0 = sharp's own limit |
4. Application secrets and keys#
All generated per workspace by provision-workspace.sh (openssl rand -hex 32). Rotation is manual and some keys
cannot be lost - see secrets and keys.
| Variable |
Used by |
Secret |
Protects |
MIGRATION_SOURCE_ENCRYPTION_KEY |
core-api |
yes |
credentials of saved Pimcore migration sources |
GATEWAY_AUTH_ENCRYPTION_KEY |
gateway, core-api |
yes |
upstream credentials of gateway endpoints |
SITE_API_KEY_PEPPER |
core-api and gateway (same value) |
yes |
hashing of per-website site keys; changing it invalidates every site key |
TOTP_ENCRYPTION_KEY |
core-api |
yes |
users' TOTP secrets; unset = clear text with a warning; lost = everyone re-enrols |
MAIL_SETTINGS_ENCRYPTION_KEY |
core-api |
yes |
SMTP password and mail-provider API keys; unset = they cannot be saved |
INTEGRATION_SECRETS_ENCRYPTION_KEY |
core-api (and the tools that read the settings) |
yes |
AI provider keys (text, image, translation), the Figma token and the push provider (OneSignal) key saved in the dashboard (F-055); unset = they stay in clear text with a warning; lost = enter them again |
UPLOAD_TOKEN_SECRET |
core-api, asset-processor |
yes |
signs the one-file upload tickets (32+ characters; placeholder or short = uploads answer 503) |
SEED_ADMIN_EMAIL |
core-api |
|
first administrator, created once |
SEED_ADMIN_PASSWORD |
core-api |
yes |
unset/empty = a random password is generated and printed once in the log |
5. HTTP hardening, limits and outbound policy#
| Variable |
Used by |
Default |
Meaning |
PUBLIC_RATE_LIMIT_PER_MINUTE |
core-api |
3000 |
all /public/* routes, per client address; 0 = off |
PUBLIC_SEARCH_RATE_LIMIT_PER_MINUTE |
core-api |
120 |
/public/search |
PUBLIC_QUERY_RATE_LIMIT_PER_MINUTE |
core-api |
600 |
/public/data-objects/query |
FORM_SUBMIT_RATE_LIMIT_PER_WINDOW |
core-api |
10 |
form submissions per address per 15 minutes |
UPLOAD_INTENT_RATE_LIMIT_PER_MINUTE |
core-api |
20 |
upload approvals per address per minute |
UPLOAD_PUT_RATE_LIMIT_PER_MINUTE |
asset-processor |
30 |
upload bodies per address per minute |
UPLOAD_PUBLIC_BASE_URL |
core-api |
derived from the request (localhost → :3002) |
the address a visitor's browser sends file bytes to; set it when that is not the same host as the page's API |
AI_RATE_LIMIT_PER_MINUTE |
core-api |
20 |
AI generation per user; 0 = off |
ENFORCE_PUBLISH_PERMISSION |
core-api |
true |
false: objects:write is enough to publish |
GATEWAY_REQUIRE_FIELD_ALLOWLIST |
gateway |
false |
true refuses old submit bindings that forward every field |
OUTBOUND_ALLOW_LOOPBACK |
safeFetch |
false in production |
|
OUTBOUND_ALLOW_PRIVATE |
safeFetch |
true |
RFC1918/ULA/CGNAT targets; set false on internet-facing installs or where authors are not fully trusted |
OUTBOUND_ALLOW_HOSTS |
safeFetch |
unset |
host names exempt from every outbound check |
WS_APP_BIND_ADDRESS |
Compose |
0.0.0.0 (the workspace scripts set 127.0.0.1) |
interface of the application ports |
WS_ADMIN_DASHBOARD_PORT, WS_GATEWAY_PORT, WS_ASSET_PROCESSOR_PORT, WS_CORE_API_PORT, WS_WHOOBE_ONE_SPIKE_PORT |
Compose, nginx rendering |
3000, 3001, 3002, 3003, 3006 |
host side of each port |
WS_MAX_BODY |
nginx rendering |
100M |
request body cap for the whole server |
WS_EDGE_LIMITS |
nginx rendering |
empty |
extra server-level nginx directives (the public sandbox sets rate limits here) |
6. Gateway#
| Variable |
Default |
Meaning |
GATEWAY_CORE_API_URL, GATEWAY_ASSET_PROCESSOR_URL |
http://$WHOOBE_HOSTNAME:3003 / :3002 |
where the gateway reaches the other services |
GATEWAY_DEFAULT_RATE_LIMIT_RPS |
10 |
per-key limit when a route sets none |
GATEWAY_RATE_LIMIT_BACKEND |
memory |
where the counters of the per-endpoint limit (/api/remote) and of the API clients (/client/v1) are kept: memory (this process) or redis (any Redis-protocol server, shared between processes). The limits of the public resolve and submit stay in the process |
GATEWAY_REDIS_URL |
unset |
with redis: redis://:password@host:6379, rediss:// for TLS. If the store cannot be reached the gateway counts in memory, logs a warning and GET /health reports rateLimit.degraded: true |
VALKEY_PASSWORD |
unset |
password of the optional valkey service of docker-compose.yml (compose profile ratelimit, no published port); put the same value in GATEWAY_REDIS_URL. provision-workspace.sh generates both |
GATEWAY_PROXY_TIMEOUT_MS |
30000 |
upstream timeout |
GATEWAY_PRESETS_PATH |
unset |
extra vendor presets (*.json), read per request |
GATEWAY_PLUGINS_PATH |
unset |
gateway plugins (code), loaded at start |
OBJECT_EVENTS_ENABLED |
on |
0 stops the object events of the instance: no event is written and the delivery worker in the gateway does not start. Read by core-api and the gateway, so keep it in the shared env file. See Object events |
OBJECT_EVENTS_CONCURRENCY |
5 |
how many event deliveries the gateway makes at once (1-50) |
OBJECT_EVENTS_LOOP_MAX |
5 |
the loop guard: events by one API client on one record within the window before the next delivery is stopped (0 = off). Read by core-api and the gateway. See Stop a loop |
OBJECT_EVENTS_LOOP_SUB_MAX |
12 |
the same per subscription and record, counting every API client (catches two flows answering each other; 0 = off) |
OBJECT_EVENTS_LOOP_WINDOW_SECONDS |
60 |
the length of that window |
OBJECT_EVENTS_SECRET_<NAME> |
unset |
the HMAC key of a subscription that names it (Signing key name in the dashboard). Only names with this prefix are ever read. Set it in the env file of the gateway; never in the dashboard |
VITE_GATEWAY_* |
|
build-time defaults of the gateway screens in the dashboard |
7. Mail, AI and integrations#
| Variable |
Default |
Meaning |
MAIL_PROVIDERS_PATH |
unset |
extra mail-provider definitions (*.json), read again when a file changes |
| AI provider keys, Figma token, SMTP settings, push provider key |
|
not environment variables: entered in the dashboard and stored encrypted in the database (INTEGRATION_SECRETS_ENCRYPTION_KEY for AI, Figma and push; MAIL_SETTINGS_ENCRYPTION_KEY for SMTP and mail providers) |
7.1 Publishing from the dashboard (publisher service)#
| Variable |
Used by |
Default |
Secret |
Meaning |
PUBLISH_RUNNER_TOKEN |
core-api and publisher (same value) |
unset: the runner routes answer 503, the publisher does not start |
yes |
shared secret the publisher uses to claim jobs from core-api (openssl rand -hex 32); Compose requires it for the publisher |
PUBLISH_ROOT |
publisher |
./published (/publish in Compose) |
|
where each run writes <slug>/releases/<job id> and the <slug>/current link |
PUBLISH_RUNNER_NAME |
publisher |
publisher@<host name> |
|
name the runner shows in the job log |
PUBLISH_POLL_MS |
publisher |
3000 |
|
how often the runner asks core-api for a queued job |
PUBLISH_JOB_TIMEOUT_MS |
publisher |
900000 (15 minutes) |
|
a job running longer is stopped and marked failed |
8. Logging#
| Variable |
Default |
Meaning |
LOG_LEVEL |
info |
fatal | error | warn | info | debug | trace; can also be switched at run time (log toggle) |
LOG_DIR |
./logs |
where rotated files are written before upload to the DAM log folder |
9. Browser builds (Vite, baked at image build time)#
Changing these needs the images rebuilt, not just a restart.
| Variable |
Meaning |
VITE_ASSET_PROCESSOR_URL, VITE_CORE_API_URL, VITE_GATEWAY_URL |
base URLs the browser uses (per-port in development; the bare host behind the proxy) |
VITE_BASE_URL |
URL prefix of the editor (/whoobe-one/ behind the proxy) |
VITE_WHOOBE_ONE_URL |
where the dashboard finds the editor when it is not the default |
VITE_FONT_FAMILIES |
fonts offered in the editor |
EDITOR_FRAME_ANCESTORS (runtime, editor image) |
who may embed the editor in a frame (frame-ancestors) |
10. Public sandbox#
| Variable |
Meaning |
SANDBOX_MODE |
1 turns AI, mail, imports, CDN mirror, PWA generation and outbound HTTP off |
SANDBOX_RESET_AT |
time shown in the reset banner |
SANDBOX_ALLOW_HOSTS |
host names outbound HTTP may still reach |
Set by provision-workspace.sh --profile sandbox (sandbox).
| Variable |
Used by |
Meaning |
BACKUP_AGE_RECIPIENT, BACKUP_GPG_RECIPIENT, AGE_IDENTITY |
backup and restore scripts |
encryption recipient / decryption identity (encryption) |
RESET_USER_EMAIL, RESET_USER_PASSWORD, RESET_USER_CLEAR_MFA |
packages/core/src/db/scripts/reset-user-password.ts |
break-glass password and MFA reset |
PIMCORE_DB_HOST, PIMCORE_DB_PORT, PIMCORE_DB_NAME, PIMCORE_DB_USER, PIMCORE_DB_PASSWORD, PIMCORE_ASSETS_TABLE, PIMCORE_DOCUMENTS_TABLE, PIMCORE_DOCUMENTS_ELEMENTS_TABLE, PIMCORE_VAR_ASSETS_PATH, PIMCORE_VAR_CLASSES_PATH |
packages/migration-tool |
source of a Pimcore import (password is secret) |
DATABASE_URL, PORTABILITY_TEST_DATABASE_URL |
tests and tools |
connection string for integration tests |
BASE_URL, CMS_API_URL, CMS_WEBSITE_ID, CMS_SITE_API_KEY, CMS_REVALIDATE, NEXT_PUBLIC_*, PUBLIC_SITE_URL, CORE_API_URL, GATEWAY_URL, VITE_X, VUE_APP_X |
generated sites and templates (packages/publish, SDK adapters) |
configuration of a published site, not of the CMS; CMS_SITE_API_KEY is secret and must stay on the site's server |
12. What is not configurable by variable#
Session lifetime (7 days), login lockout thresholds (5 / 25 failures, 15 minutes), MFA throttles, the form-upload ticket
lifetime (10 minutes) and staging lifetime (2 hours), password length (8) and the pool size of the database
connection are constants in the code.