Configuration reference

Deckgauge declares its configuration in .env.example. Copy it to .env and its defaults produce a working local install — but on the Docker path, declared in .env.example and reaches a container are two different things, and the section below draws that line before the tables.

deckgauge · docker-compose.yml
interpolated from .envPOSTGRES_USER: ${POSTGRES_USER:-cockpit} — your .env value wins; falls back to cockpit when unset.
interpolated from .envNEXTAUTH_SECRET: "${NEXTAUTH_SECRET:-deckgauge-nextauth-secret}" — same pattern; set it in .env to rotate it.

Every secret-shaped value is interpolated from .env, falling back to a published default when unset.

How configuration reaches each container

docker compose reads .env from the repository root automatically and interpolates ${VAR:-default} references inside docker-compose.yml, so a variable can be absent from your .env and still resolve to a sane default. What it does not do is hand a container the whole file: docker-compose.yml carries no env_file: directive on any service. A line in .env therefore reaches a container only if compose interpolates it into that service's environment: block.

Every secret-shaped value on every service is interpolated that way: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB; CLICKHOUSE_USER, CLICKHOUSE_PASSWORD (compose-only — they aren't in .env.example); MICROSOFT_TENANT_ID, MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET; KEYCLOAK_CLIENT_SECRET and NEXTAUTH_SECRET on the web service; and Keycloak's own database and admin-console credentials — KEYCLOAK_DB_NAME, KEYCLOAK_DB_USER, KEYCLOAK_DB_PASSWORD, KEYCLOAK_ADMIN_USER, KEYCLOAK_ADMIN_PASSWORD — on the keycloak-db and keycloak services. Setting any of those in .env changes what the container gets; each still falls back to the value published on this page when unset, so an existing install with no .env at all, or one whose .env leaves these lines out, keeps working exactly as before. Everything else a container sees — the Keycloak URLs, the client id, NEXTAUTH_URL, hostname settings — is a non-secret literal written into docker-compose.yml; edit docker-compose.yml itself to change one of those.

Upgrading? One sign-out, onceThe standard setup step is cp .env.example .env, and .env.example ships NEXTAUTH_SECRET=change-me-in-production. That line now interpolates into the web service instead of being ignored, so on an install that followed that step the session-signing secret changes from the old compose fallback to change-me-in-production the first time you bring the stack up after upgrading. Every existing browser session is invalidated and everyone signs in once more; nothing else is affected, and it does not repeat. Since you are editing .env anyway, set a real NEXTAUTH_SECRET now rather than leaving the published placeholder in place.
Change the defaults before exposing an instanceThe values above resolve to a working default when .env doesn't set them, and those defaults are published on this page — fine for a private local install, not fine once anyone outside your machine can reach it. Set KEYCLOAK_CLIENT_SECRET, NEXTAUTH_SECRET, KEYCLOAK_ADMIN_PASSWORD, and KEYCLOAK_DB_PASSWORD in .env before then (and match the client secret in Keycloak's admin console).

The tables below list what each variable does. Where a variable is declared in .env.example but doesn't reach a container on the Docker path, the row says so; those still apply when you run apps/api, apps/web, or apps/worker directly on the host, which reads .env normally. A few rows below aren't in .env.example at all — either a compose-only literal or a variable the code reads with a working default — and each of those says so too.

Variables by service

Postgres & database

VariableWhat it doesRequired?
POSTGRES_USERPostgres role Deckgauge connects asNo — defaults work locally
POSTGRES_PASSWORDPassword for that roleNo — defaults work locally
POSTGRES_DBDatabase name (cockpit)No — defaults work locally
DATABASE_URLFull Postgres connection string Prisma and the API useYes
DATABASE_TEST_URLSeparate connection string for integration testsOnly if running the test suite

Redis

VariableWhat it doesRequired?
REDIS_URLRedis connection string backing the BullMQ queues the worker runsYes

ClickHouse

Not declared in .env.example at all. On the Docker path it's a compose literal on both the api and worker services, built from CLICKHOUSE_USER/CLICKHOUSE_PASSWORD (compose-only, same as noted above).

VariableWhat it doesRequired?
CLICKHOUSE_URLConnection string the API and worker use for engineering-intelligence queries and dual-writes. Falls back to http://cockpit:cockpit@localhost:8123/cockpit when unset — the default only matters when running apps/api or apps/worker on the hostNo — defaults work locally

Jira & Azure DevOps sync

VariableWhat it doesRequired?
JIRA_CONFIG_PATHDeclared in .env.example and passed to the api and worker containers, but nothing reads it — Jira connections live in the database and are set up from a board's Sources tab, not from a fileNo — inert; nothing breaks if you drop it
ADO_CONFIG_PATHPath to the Azure DevOps connection config, resolved relative to the repo root by the workerOnly for the file-configured ADO path
USE_FAKE_AZURE_DEVOPSSet to true, the worker's work-item adapter returns fake data instead of calling the real API. Its PR and commit adapters gate on a separate USE_FAKE_ADO. Note that .env.example ships this switched on, and that compose passes neither variable to the worker container — both only bite when you run the worker on the hostNo — dev/test convenience

Sync scheduling

Not declared in .env.example and not passed through docker-compose.yml to the worker service — on the Docker path this always runs at its default. It only takes effect when you run apps/worker on the host with it set directly.

VariableWhat it doesRequired?
CRON_INTERVALBase repeat interval, in milliseconds, for every scheduled sync and intelligence job the worker runs — Jira, GitHub, GitLab and Azure DevOps sync, plus Jira/ADO intelligence sync (the daily prune queue is hardcoded separately and ignores it)No — defaults to 900000 (15 minutes)

Per-organization concurrency

Declared in .env.example (commented out) and interpolated into the worker service, so unlike CRON_INTERVAL above this one does reach the container on the Docker path.

VariableWhat it doesRequired?
WORKER_MAX_CONCURRENT_JOBS_PER_ORGMaximum sync jobs a single organization may run at once on the worker's tier queues. When one organization hits the cap, its next job is put back on the delayed set instead of holding a worker slot, which is what lets a quieter organization's job run instead of queueing behind it. Zero or unset disables the cap entirelyNo — defaults to 0 (disabled)
Leave it unset unless you host several organizationsDisabled is the default precisely so that a single-organization install — every self-host, and the open-source distribution — behaves exactly as it did before the cap existed. With one organization there is nobody to be fair to, and a cap can only slow you down.
The cap counts per worker process, not per clusterIt is an in-process counter. The standard deployment runs one worker container, so per-process and cluster-wide are the same number today. If you scale the worker to two replicas, the effective cap per organization doubles — plan for that rather than discovering it.

Web / API base URL

VariableWhat it doesRequired?
API_URLNot declared in .env.example. Server-side base URL the web app's Next.js server actions use to call the API directly — distinct from the browser-facing variable below. A compose literal (http://api:3001) on the Docker path; falls back to http://localhost:3001 when unsetNo — defaults work locally
NEXT_PUBLIC_API_URLAPI base URL the browser calls directly (used for uploads)Yes

Keycloak (identity)

VariableWhat it doesRequired?
KEYCLOAK_ISSUERRealm issuer URL the API verifies incoming JWTs againstYes
KEYCLOAK_CLIENT_IDOIDC client id the web app authenticates asYes
KEYCLOAK_CLIENT_SECRETSecret for that client. Interpolated from .env into the web service's compose environment (falls back to the published deckgauge-secret when unset) — set it in .env to rotate it, and update the matching client secret in Keycloak's admin console to keep them in syncYes
KEYCLOAK_JWKS_URIWhere the API fetches Keycloak's signing keysYes
KEYCLOAK_INTERNAL_URLNot declared in .env.example. Docker-internal Keycloak URL (container→container) the web app uses server-side for the token/userinfo/JWKS calls, since a container can't reach the browser-facing KEYCLOAK_ISSUER (localhost:8080) — a compose literal on the Docker path. Falls back to KEYCLOAK_ISSUER when unset, which is the correct behaviour on the host-run path, where localhost:8080 is directly reachableNo — Docker-only; leave it unset on the host
NEXT_PUBLIC_KEYCLOAK_ISSUERRemoved. NEXT_PUBLIC_* is inlined into the browser bundle at build time, so it described the machine that built the image rather than the install running it — sign-out sent the id_token to the wrong realm. Login and logout now read KEYCLOAK_ISSUER at runtimeNo — no longer read
NEXT_PUBLIC_KEYCLOAK_CLIENT_IDRemoved for the same reason; KEYCLOAK_CLIENT_ID covers itNo — no longer read
KEYCLOAK_DB_NAMENot in the Keycloak section of .env.example's comments but declared alongside the DB vars below. Database name for Keycloak's own Postgres instance (the keycloak-db service) — interpolated into both keycloak-db and keycloak's compose environment, falling back to keycloakNo — defaults work locally
KEYCLOAK_DB_USERRole Keycloak connects to its own database as — same interpolation, falls back to keycloakNo — defaults work locally
KEYCLOAK_DB_PASSWORDPassword for that role — same interpolation, falls back to the published keycloak. Change it before exposing an instance beyond localhostNo — defaults work locally, but rotate before exposing an instance
KEYCLOAK_ADMIN_USERUsername for Keycloak's own admin console — interpolated into the keycloak service, falls back to adminNo — defaults work locally
KEYCLOAK_ADMIN_PASSWORDPassword for the Keycloak admin console — same interpolation, falls back to the published admin. Change it before exposing an instance beyond localhostNo — defaults work locally, but rotate before exposing an instance

NextAuth.js

VariableWhat it doesRequired?
NEXTAUTH_SECRETSigns the session. Interpolated from .env into the web service's compose environment (falls back to the published deckgauge-nextauth-secret when unset) — set it in .env before any real deploymentYes
NEXTAUTH_URLCanonical URL NextAuth uses for callbacksYes

API rate limiting

Neither variable is declared in .env.example, and neither is passed through docker-compose.yml to the api service — on the Docker path the limiter always runs at its defaults. Setting them has an effect only when you run apps/api on the host with them set directly.

VariableWhat it doesRequired?
RATE_LIMIT_MAXMaximum requests per window before the API's global rate limiter (@fastify/rate-limit, applied to every route) starts returning 429No — defaults to 300
RATE_LIMIT_WINDOWWindow length for that cap, e.g. 1 minuteNo — defaults to 1 minute

API authorization

Not declared in .env.example, and not passed through docker-compose.yml to the api service — on the Docker path the API always runs fully enforced. Setting any of these has an effect only when you run apps/api on the host with them set directly, or after adding them to docker-compose.yml's api service yourself.

VariableWhat it doesRequired?
DECKGAUGE_SINGLE_USERSet to true, every API request is allowed with no authentication or authorization check at all — every declared policy (board role, org-tree role, comparison, connection ownership, everything) is bypassed, not just the checks a single-user setup would plausibly need. Logs a warning on startup so an instance left this way isn't silently open. Meant for a genuinely single-user install nobody else can reachNo — defaults to off (fully enforced)
COCKPIT_ADMIN_ROLEName of the Keycloak realm role that grants admin access — salary read/write, Owner-equivalent on every org tree, and (see below) the analytics role tooNo — defaults to cockpit-admin
COCKPIT_ANALYTICS_ROLEName of the Keycloak realm role required to read engineering-intelligence data; timesheet reads also require Viewer on the org tree named in the request — see Access controlNo — defaults to cockpit-analytics

Both realm roles are declared in the shipped keycloak/realm-export.json (so a fresh realm import creates them ready to assign from Keycloak's admin console) but nobody holds either one on a fresh install — you assign them to a user from Keycloak's admin console under that user's Role mapping.

Org-tree access starts emptyPer-tree OrgTreeAccess rows (Viewer/Editor/Owner on a specific org tree) don't exist until someone creates them. On an instance with no admin account yet, run the recovery script from a shell with access to the database: DATABASE_URL=<your Postgres URL> pnpm --filter @deckgauge/db grant:org-access --email [email protected] --tree all --role OWNER. It looks the user up by email (they must have signed in at least once so a local User row exists), grants the given role — OWNER, EDITOR, or VIEWER — on one tree id or every tree with --tree all, and prints what it changed. This is the documented recovery path precisely so nobody has to hand-write SQL against production. See Access control for the full org-tree role model.

Microsoft Entra ID SSO

Leave all three blank to disable Microsoft sign-in entirely — see Keycloak and identity providers for what happens when they're only partially set. The worker also reads these same three for the delegated-refresh-token fallback in Microsoft Graph directory sync below.

VariableWhat it doesRequired?
MICROSOFT_TENANT_IDEntra tenant id for the SSO identity provider, and for the org-tree Graph sync's delegated-refresh-token fallbackNo — optional
MICROSOFT_CLIENT_IDEntra app registration's client id — same dual useNo — optional
MICROSOFT_CLIENT_SECRETSecret for that app registration — same dual useNo — optional

Microsoft Graph directory sync

An org tree's primary connection is a pasted Graph Explorer access token, set from the org tree's Source tab — see Syncing your org directory from Microsoft Graph. The MICROSOFT_* variables above are read only as a fallback, for the delegated device-code flow.

VariableWhat it doesRequired?
GRAPH_TENANT_IDDeclared in .env.example, but read nowhere in the codebase — org-tree Graph sync uses MICROSOFT_TENANT_ID insteadNo — unused
GRAPH_CLIENT_IDDeclared in .env.example, but read nowhere — use MICROSOFT_CLIENT_IDNo — unused
GRAPH_CLIENT_SECRETDeclared in .env.example, but read nowhere — use MICROSOFT_CLIENT_SECRETNo — unused
USE_FAKE_GRAPHRuns the worker against a fake Graph client instead of the real APINo — dev/test convenience

Advisor (board Q&A)

VariableWhat it doesRequired?
ADVISOR_PROVIDERanthropic or ollama — leave blank and the panel reports itself unconfiguredNo — optional
ADVISOR_MODELModel name, e.g. claude-haiku-4-5 or llama3.2Only with ADVISOR_PROVIDER set
ADVISOR_ANTHROPIC_API_KEYAnthropic-only key; a bare ANTHROPIC_API_KEY is never picked upOnly for the Anthropic provider
ADVISOR_OLLAMA_BASE_URLOllama-only base URL, including scheme (e.g. http://localhost:11434)Only for the Ollama provider
ADVISOR_BRIDGE_AUTOSTARTStarts the local-agent bridge alongside every deployNo — optional
DECKGAUGE_TOKENRuns the advisor bridge headless, with no browser involvedNo — optional

Open-core edition & licensing

VariableWhat it doesRequired?
DECKGAUGE_EDITIONcommunity (default) or enterpriseNo — defaults to Community
DECKGAUGE_ENTERPRISE_MODULEAbsolute path to the built enterprise module entry pointOnly for the Enterprise edition
DECKGAUGE_LICENSE_FILEAbsolute path to the signed license fileOnly for the Enterprise edition
DECKGAUGE_ALLOW_DEV_LICENSE_KEYSTrusts an extra verify key so locally-signed dev licenses validate — never set to 1 in productionNo — dev/test only
DECKGAUGE_LICENSE_PUBKEYThe extra dev verify key trusted when the flag above is onNo — dev/test only

If it looks wrong

SymptomCauseFix
Login page shows no Microsoft option, though you set the variablesThe realm import is only reprocessed on the realm's first boot — once it exists in the persisted keycloak-db volume, recreating the keycloak container doesn't reliably reprocess itAdd the Microsoft identity provider directly from the Keycloak admin console — see Keycloak and identity providers for why a recreate isn't the fix
Advisor panel says it's unconfiguredADVISOR_PROVIDER is blank and no local-agent bridge is connectedSet the Advisor variables, or configure a provider from Settings — see Configuring an LLM provider
API starts as Community even with DECKGAUGE_EDITION=enterpriseDECKGAUGE_ENTERPRISE_MODULE is unset or points at a path that doesn't existSet it to the module's absolute path — see Community and Enterprise editions
Every request succeeds even with no login at allDECKGAUGE_SINGLE_USER=true is set somewhere the API reads itUnset it (or set it to anything other than true) unless this really is a single-user instance nobody else can reach

Related

Last updated