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_DOMAIN—http://localhost:3000or your public URLJWT_SECRET— any random string (the default is a known value)ADMIN_EMAIL— Pro/Max only; Personal tier needs nothing more
Build your config
Pick what you're running; the command, compose file and boot previews update as you type.
Tier
Database
Pro/Max featureFile storage
Pro/Max featureAI 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==============================================================================
UnDercontrol v1.x.x is ready
--> Open http://localhost:3000 to get started
Login as: personal@undercontrol.local
default password: personal123 (set PERSONAL_TIER_PASSWORD to change it)
Tier: Personal (max users: 1)
Database: SQLITE
Storage: LocalFS
==============================================================================Boot preview — misconfigured
HOST_DOMAIN missing==============================================================================
STARTUP FAILED: configuration error
HOST_DOMAIN is required: set it to the external base URL clients use to
reach this backend (scheme + host, no trailing slash), e.g.
HOST_DOMAIN=https://app.example.com or HOST_DOMAIN=http://localhost:8080.
==============================================================================Full reference
Click a variable name to copy it.
Core server
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.
--host-domainPort 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.
8080--portdevelopment or production. Production switches the HTTP framework to release mode (less request logging). The all-in-one image ships with production preset.
development--environmentLog verbosity: debug, info, warn, error. Logs are structured JSON; the human-readable boot banner prints regardless of level.
info--log-levelSQL statement logging: silent, error, warn, or info (logs every query — debugging only).
silent--sql-log-levelDirectory 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.
./data--data-pathAccounts & authentication
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.
your-secret-key--jwt-secretAccess-token lifetime in minutes. Sessions refresh automatically, so the practical effect is how long a stolen token stays valid.
60--jwt-expiration-minutesLogin username of the initial admin account, created at first boot. On Pro/Max the server refuses to boot without it — Personal tier ignores it.
--admin-emailInitial 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).
admin123--admin-passwordPassword 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.
personal123--personal-tier-passwordOne-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.
false--migrate-from-personalLicensePersonal tier needs none of these
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.
--license-tokenHost secret paired with your license token; both are issued together.
--license-host-secretUsed for issuing licenses. Leave unset on self-hosted instances.
--license-private-keyDatabaseSQLite 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.
sqlite--database-typeFull PostgreSQL connection string. When set, it overrides all the individual POSTGRES_* variables below.
--database-urlIndividual 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).
--postgres-host, --postgres-port, …Connection-pool tuning; the defaults suit a small instance: max open 25, max idle 5, connection lifetime 300 s, idle timeout 60 s.
--postgres-max-open-conns, …Files & storagelocal disk by default — S3 is Pro/Max
Upload size limit in bytes.
10485760 (10 MB)--max-file-sizeStore 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.
false--s3-enabledAll 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.
--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.
--openai-api-keyModel 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.
--openai-model, --openai-base-url, …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.
false / false--ai-startup-backend-active, …Azure computer-vision credentials for image analysis, if you use Azure instead of an OpenAI-compatible vision model.
--azure-vision-key, --azure-vision-urlExternal OCR service for receipt/document text extraction: endpoint (http://127.0.0.1:8000/ocr), auth token, timeout in seconds (30).
--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.
http://localhost:3000,http://localhost:12000,http://localhost:8080--cors-allowed-originsWhere 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.
http://localhost:12000--frontend-urlSlack incoming-webhook for server notifications such as backup results.
--slack-webhook-urlBackground jobs
Master switch for scheduled jobs (cleanup, backups, scheduled-task processing). Leave on unless you run a separate worker instance.
true--cron-enabledAutomatic removal of expired visitor (demo) accounts and their data: on by default, retention 3 days, runs daily at midnight (0 0 * * *, cron syntax).
--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.
false--otel-enabledStandard 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).
--otel-endpoint, …Advancedrarely needed
Internal event-bus sizing: background workers (4) and queue capacity (1000). Only worth touching on unusually busy instances.
--event-bus-workers, --event-bus-queue-sizeMaintained 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.