Skip to main content

UnDercontrol · Self-hosting

Configuration Reference

Build a working configuration below, then look anything up in the full reference. Every setting can be given three ways, in order of precedence: CLI flag › environment variable › built-in default. A .env file in the working directory is loaded automatically.

Source of truth: go-backend/internal/config/config.go — this page tracks it.

The minimum to boot

  • HOST_DOMAINhttp://localhost:3000 or your public URL
  • JWT_SECRET — any random string (the default is a known value)
  • ADMIN_EMAIL — Pro/Max only; Personal tier needs nothing more

Get these wrong and the server won't limp along — it exits with a STARTUP FAILED block naming the variable to fix. Get them right and the log ends with --> Open <url> to get started.

Build your config

Pick what you're running; the command, compose file and boot previews update as you type.

Tier

Database

Pro/Max feature

File storage

Pro/Max feature

AI features

HOST_DOMAIN

The URL browsers will use to reach the instance — the misconfigured preview below shows what happens when it is missing or invalid.

JWT_SECRET

Random value generated for you —

docker run -d --name undercontrol \
  -p 3000:8080 \
  -e HOST_DOMAIN=http://localhost:3000 \
  -e JWT_SECRET=d48057569ddb338941c0219752990ca49e3bcb11df845eab \
  -v undercontrol-data:/app/data \
  lintao0o0/undercontrol:latest

Boot preview — success

docker logs undercontrol

Boot preview — misconfigured

HOST_DOMAIN missing

Full reference

Click a variable name to copy it.

Core server

Required

External base URL clients use to reach this instance — scheme + host, no trailing slash (e.g. https://ud.example.com, or http://localhost:3000 when mapping -p 3000:8080). Used to build file download/upload links, so it must be the address browsers actually use. Missing or malformed → the server refuses to boot.

DefaultFlag--host-domain

Port the server listens on. In the all-in-one image, this is the port inside the container — map it with -p <host>:8080. If the port is already taken, boot fails fast with a clear error.

Default8080Flag--port

development or production. Production switches the HTTP framework to release mode (less request logging). The all-in-one image ships with production preset.

DefaultdevelopmentFlag--environment

Log verbosity: debug, info, warn, error. Logs are structured JSON; the human-readable boot banner prints regardless of level.

DefaultinfoFlag--log-level

SQL statement logging: silent, error, warn, or info (logs every query — debugging only).

DefaultsilentFlag--sql-log-level

Directory holding all local state: the SQLite database and locally-stored uploads. In Docker this is /app/data — mount a volume there; backing up this directory backs up the instance.

Default./dataFlag--data-path

Accounts & authentication

Set it — default is public

Secret used to sign login tokens. The default is a publicly known placeholder — anyone who knows it can forge sessions. Always set a long random value on any instance that is not throwaway.

Defaultyour-secret-keyFlag--jwt-secret

Access-token lifetime in minutes. Sessions refresh automatically, so the practical effect is how long a stolen token stays valid.

Default60Flag--jwt-expiration-minutes
RequiredPro / Max

Login username of the initial admin account, created at first boot. On Pro/Max the server refuses to boot without it — Personal tier ignores it.

DefaultFlag--admin-email
Pro / Max

Initial admin password, applied only when the admin account is first created. Change it after first login (the boot banner reminds you while the default is in use).

Defaultadmin123Flag--admin-password
PersonalSet before first boot

Password of the single Personal-tier user, personal@undercontrol.local (the login name itself cannot be changed). The Start auto-login always uses this variable, but the stored password is only written at first boot — changing one side without the other breaks auto-login. Set it before the first start, or keep both in sync.

Defaultpersonal123Flag--personal-tier-password
Pro / Max

One-shot migration switch. Starting a Pro/Max server against a database created by Personal tier is refused as a safety check; set this to true (together with ADMIN_EMAIL / ADMIN_PASSWORD) to transfer all data to the new admin account and remove the personal user.

DefaultfalseFlag--migrate-from-personal

LicensePersonal tier needs none of these

Pro / Max

License token that unlocks Pro/Max features (multi-user, PostgreSQL, S3, admin dashboard). Also read from UNDERCONTROL_LICENSE, a license.txt in the working directory, or /etc/undercontrol/license.txt — first match wins.

DefaultFlag--license-token
Pro / Max

Host secret paired with your license token; both are issued together.

DefaultFlag--license-host-secret

Used for issuing licenses. Leave unset on self-hosted instances.

DefaultFlag--license-private-key

DatabaseSQLite by default — PostgreSQL is Pro/Max

sqlite or postgres. SQLite lives inside UD_DATA_PATH and is enough for most single-household instances; PostgreSQL requires a Pro/Max license.

DefaultsqliteFlag--database-type

Full PostgreSQL connection string. When set, it overrides all the individual POSTGRES_* variables below.

DefaultFlag--database-url
/ PORT / USER / PASSWORD / DATABASE / SSL_MODE

Individual connection parts, for when you prefer them over one URL: POSTGRES_HOST (localhost), POSTGRES_PORT (5432), POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DATABASE (undercontrol), POSTGRES_SSL_MODE (disable).

Flag--postgres-host, --postgres-port, …
/ MAX_IDLE_CONNS / CONN_MAX_LIFETIME / CONN_MAX_IDLE_TIME

Connection-pool tuning; the defaults suit a small instance: max open 25, max idle 5, connection lifetime 300 s, idle timeout 60 s.

Flag--postgres-max-open-conns, …

Files & storagelocal disk by default — S3 is Pro/Max

Upload size limit in bytes.

Default10485760 (10 MB)Flag--max-file-size
Pro / Max

Store uploads in S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) instead of the local data directory. The boot banner shows which mode is active.

DefaultfalseFlag--s3-enabled
/ REGION / BUCKET / ACCESS_KEY_ID / SECRET_ACCESS_KEY / FORCE_PATH_STYLE

All required once S3_ENABLED=true: endpoint URL, region (auto works for R2), bucket name, credentials. S3_FORCE_PATH_STYLE (default true) suits MinIO and most non-AWS providers.

Flag--s3-endpoint, --s3-bucket, …

AI & visionall optional — AI features stay off without a key

API key that enables the AI features (task-from-image, receipt extraction, chat). Works with any OpenAI-compatible endpoint — pair with OPENAI_BASE_URL for Azure, GitHub Models, or a local model server.

DefaultFlag--openai-api-key
/ BASE_URL / MAX_TOKENS / TEMPERATURE / ORG_ID

Model and endpoint tuning: model name (gpt-3.5-turbo), base URL (https://api.openai.com/v1), response cap (1000 tokens), temperature (0.7), optional organization ID.

Flag--openai-model, --openai-base-url, …
/ AI_STARTUP_FRONTEND_ACTIVE

Whether the system AI provider configured above starts out active on the backend / offered to the frontend. Users can still bring their own provider in-app.

Defaultfalse / falseFlag--ai-startup-backend-active, …
/ AZURE_VISION_URL

Azure computer-vision credentials for image analysis, if you use Azure instead of an OpenAI-compatible vision model.

DefaultFlag--azure-vision-key, --azure-vision-url
/ OCR_AUTHORIZATION / OCR_TIMEOUT

External OCR service for receipt/document text extraction: endpoint (http://127.0.0.1:8000/ocr), auth token, timeout in seconds (30).

Flag--ocr-endpoint, …

Network & frontend

Comma-separated list of origins allowed to call the API from a browser. Only needed when the frontend is served from a different origin than the backend — the all-in-one image serves both from one origin, so the default is fine there.

Defaulthttp://localhost:3000,http://localhost:12000,http://localhost:8080Flag--cors-allowed-origins

Where shareable links point when generated outside a browser (e.g. from the desktop app). Set it to the same public URL as HOST_DOMAIN for all-in-one deployments.

Defaulthttp://localhost:12000Flag--frontend-url

Slack incoming-webhook for server notifications such as backup results.

DefaultFlag--slack-webhook-url

Background jobs

Master switch for scheduled jobs (cleanup, backups, scheduled-task processing). Leave on unless you run a separate worker instance.

DefaulttrueFlag--cron-enabled
/ VISITOR_RETENTION_DAYS / VISITOR_CLEANUP_SCHEDULE

Automatic removal of expired visitor (demo) accounts and their data: on by default, retention 3 days, runs daily at midnight (0 0 * * *, cron syntax).

Flag--visitor-cleanup-enabled, …

Observabilityfor operators who ship logs/traces elsewhere

Export traces, metrics and logs via OpenTelemetry (OTLP). Off by default; logs still go to stdout either way.

DefaultfalseFlag--otel-enabled
/ _HEADERS / TRACES / METRICS / LOGS / SERVICE_NAME

Standard OTLP exporter settings: one shared endpoint (OTEL_EXPORTER_OTLP_ENDPOINT), auth headers (OTEL_EXPORTER_OTLP_HEADERS), per-signal overrides (OTEL_TRACES_ENDPOINT / OTEL_METRICS_ENDPOINT / OTEL_LOGS_ENDPOINT), and the reported service name (OTEL_SERVICE_NAME).

Flag--otel-endpoint, …

Advancedrarely needed

/ EVENT_BUS_QUEUE_SIZE

Internal event-bus sizing: background workers (4) and queue capacity (1000). Only worth touching on unusually busy instances.

Flag--event-bus-workers, --event-bus-queue-size

Maintained by the Onboarding Experience Owner. When a variable is added, renamed, or its default changes in config.go, this page changes in the same task — if you find drift, that is a bug worth reporting. See also the Self-Deployment Guide.