Skip to content

Configuration

View markdown

Every environment variable KDIGI CMS reads, what it does, and where it is used. Copy .env.example to .env for local development; in Docker/Coolify set variables on the container (deploy). Never commit a real .env.

Only three values are required: DATABASE_URI, PAYLOAD_SECRET, PAYLOAD_PUBLIC_SERVER_URL. With docker compose up you need none of them (the stack wires the database and generates a secret).

VariableRequiredDefaultDescriptionRead in
DATABASE_URIyes—PostgreSQL connection string, e.g. postgres://kdigi:change-me@db:5432/kdigi. Pool: max 20 connections, 10 s connect timeout, 120 s idle-in-transaction timeout.src/payload.config.ts
PAYLOAD_SECRETyes—Signs sessions and API keys. ≥ 32 characters in production or the server refuses to boot. Changing it logs everyone out. The Docker entrypoint generates one into DATA_DIR if unset.src/payload.config.ts, docker/entrypoint.sh
PAYLOAD_PUBLIC_SERVER_URLyes (prod)http://localhost:3000Public URL of the CMS as browsers see it (https://cms.example.com). It is the admin origin used for CORS/CSRF and the prefix of media URLs. A wrong value shows up as 403 / lost login cookies. In development, localhost:3000/3001/3101 are also accepted.src/payload.config.ts
NODE_ENVnoset by Nextproduction enables the secret length check, boot warnings and disables the GraphQL playground.several
PAYLOAD_CONFIG_PATHnoset in DockerfilePath of payload.config.ts for the Payload CLI inside the image (/app/src/payload.config.ts).Payload CLI

Local disk is the default; see media and storage.

VariableDefaultDescriptionRead in
MEDIA_DIR./media (Docker: /app/media)Directory for uploads of every site that has no Cloudflare R2 configuration. Mount a persistent volume here. Layout: <MEDIA_DIR>/<siteId>/<filename>.src/lib/storage/local-media.ts
R2_OFFunsettrue forces local disk for all sites, even those with R2 settings (useful when developing against a copy of production data).src/lib/r2-bucket-resolver.ts

R2 credentials are not environment variables: they are configured per site in the admin (Sites → System → Cloudflare R2), so each site can use its own bucket.

VariableDefaultDescriptionRead in
ENABLE_JOBS_AUTORUNunsettrue starts the job runner that executes queued "revalidate frontend" jobs (every 10 s, up to 25 jobs per pass) and recovers jobs stuck in processing for more than 5 minutes after a restart. Set it on exactly one instance — Payload has no cross-instance job lock. Without it, jobs are queued but never run and production logs a warning at boot.src/payload.config.ts
FRONTEND_BASE_URLunsetPublic base URL of a frontend, used to build document URLs for the admin "view on site" links. Fallback chain: this value → https://<Sites.domain> → http://localhost:3002. It applies to every site, so leave it unset on a multi-site install and let each site's domain decide. It is not the revalidation target; revalidation calls https://<Sites.domain>/api/cms/revalidate per site (the manual button falls back to http://localhost:3002 only outside production).src/endpoints/frontend-doc-url.ts, src/endpoints/revalidate-frontend.ts
REVALIDATE_SECRETunsetWhen set, every revalidate ping (job and the editor's manual button) carries the header x-revalidate-secret: <value>, so a frontend can reject pings that do not come from this CMS. One value for all sites; unset keeps the ping unauthenticated.src/lib/revalidate-headers.ts
VariableDefaultDescriptionRead in
DEEPSEEK_API_KEYunsetAPI key for the DeepSeek API (OpenAI-compatible, base URL https://api.deepseek.com). Without it the AI SEO buttons return an error; nothing else is affected.src/lib/ai/deepseek-client.ts
DEEPSEEK_MODELunsetModel id to call, e.g. deepseek-chat. There is no built-in default — both variables are needed to use AI generation. Check a model with pnpm verify:deepseek.src/lib/ai/deepseek-client.ts
SEO_AI_RATE_LIMIT20Maximum AI SEO generations per user per minute (in-memory, per instance).src/endpoints/seo-ai-generate.ts

Run a public sandbox login (for example a read-me-first demo editor) without letting visitors lock each other out. Fully opt-in: with DEMO_ACCOUNT_EMAILS unset nothing changes. Role and site boundaries still come from roles and permissions.

VariableDefaultDescriptionRead in
DEMO_ACCOUNT_EMAILSunsetComma-separated emails (case-insensitive). These users cannot change their own email, password, API key or API-key switch (the request is rejected with a bilingual 403 message) and cannot delete themselves. Other users, including a super-admin, can still manage them.src/lib/demo-accounts.ts, src/collections/users.ts
DEMO_ACCOUNT_PASSWORDunsetPassword that pnpm demo:reset sets on every account in DEMO_ACCOUNT_EMAILS (min. 8 characters). Required by that command only.src/seed/demo-reset.ts

pnpm demo:reset (in the Docker image it works as is; equivalent: node --import tsx src/seed/demo-reset.ts) wipes only the site with siteKey = demo (all its content, media files and folders, trash included), re-seeds it with the same data as pnpm seed:demo, and for each demo account creates it if missing and resets its password, role (editor) and sites (demo only). Other sites and users are never touched. Schedule it nightly, e.g. as a Coolify scheduled task on the app service with cron 0 3 * * * and timezone Asia/Ho_Chi_Minh.

VariableDefaultDescriptionRead in
RECAPTCHA_SECRETunsetGoogle reCAPTCHA secret used to verify the recaptchaToken sent to POST /api/comments/create. If unset, production logs a [SECURITY] warning at boot because the public comment endpoint is then open to spam.src/endpoints/create-comment.ts, src/payload.config.ts
MODERATION_WEBHOOK_URLunsetEvery new pending comment is POSTed here as JSON. Unset = disabled.src/collections/comments.ts
VariableDefaultDescriptionRead in
OTEL_SERVICE_NAMEkdigi-cmsService name on OpenTelemetry traces.src/instrumentation.ts
OTEL_EXPORTER_OTLP_ENDPOINTunsetOTLP/HTTP collector base URL; traces are sent to <url>/v1/traces. Unset = no export.src/instrumentation.ts

Read by docker/entrypoint.sh and docker-compose.yml / docker-compose.coolify.yml only:

VariableDefaultDescription
SEED_DEMOunsettrue loads the idempotent demo site and sample content after migrating (getting started).
DEMO_ACCOUNT_EMAILS, DEMO_ACCOUNT_PASSWORD, REVALIDATE_SECRETunsetForwarded by both compose files to the app container; see above.
SKIP_MIGRATIONSunsettrue skips the automatic payload migrate on container start (use when migrations run as a separate release step, see upgrading).
DATA_DIR/app/dataWhere the entrypoint persists a generated PAYLOAD_SECRET (payload-secret). Mount a volume.
POSTGRES_PASSWORDkdigi (local compose)Database password for the bundled PostgreSQL in the local compose file. The Coolify compose uses a generated SERVICE_PASSWORD_POSTGRES.
APP_PORT, DB_PORT3000, 5432Host ports of the local compose file (PostgreSQL binds to 127.0.0.1).

Only used by the importer scripts in src/importers/ and src/scripts/; see WordPress migration (Vietnamese) for the full list and behaviour: WP_URL, WP_AUTH_USER, WP_AUTH_PASSWORD, WP_POST_TYPES, WP_USER_AGENT, WP_UTC_OFFSET, GATE_APPROVER, KDIGI_LOCAL_DB_HOSTS, BACKUP_DIR.

VariableDescription
PAYLOAD_INTEGRATION=1Enables *.integration.test.ts (pnpm test:integration) against a local database.
PW_SKIP_WEB_SERVER=1Playwright (pnpm test:e2e) reuses an already running dev server.
NEXT_PUBLIC_E2E=1Exposes an editor test hook in non-production builds; never set in production.

Per-site settings (not environment variables)

Section titled “Per-site settings (not environment variables)”

Everything that differs between websites lives in the Sites collection, editable in the admin: domain, default language, branding, SEO defaults and templates, contact/footer data, Cloudflare R2 storage, Organization identity for JSON-LD. See multi-site.

  • PAYLOAD_SECRET ≥ 32 random chars, unique per install.
  • PAYLOAD_PUBLIC_SERVER_URL = the real https:// URL of the CMS.
  • ENABLE_JOBS_AUTORUN=true on one instance if a frontend uses revalidation.
  • RECAPTCHA_SECRET if the public comment endpoint is exposed.
  • Persistent volumes (or R2) for media; database backups (deploy).