Skip to content

Environment Variables Reference

fuomag9 edited this page Sep 26, 2026 · 15 revisions

Environment Variables Reference

Configuration variables for Caddy Proxy Manager, including the ones you actually need to set and the ones that just work out of the box.

Passing variables to the container: the stock docker-compose.yml passes a fixed list of variables to the web container. A variable it does not list (for example LOGIN_*, AUTH_RATE_LIMIT_*, AUTH_TRUST_HOST, AUTH_ALLOW_OAUTH_ROLE_FROM_CLAIMS, LEGACY_KEY_CUTOFF_DATE, or any INSTANCE_* variable other than INSTANCE_SYNC_TIMEOUT_MS) has no effect from .env until you add it under the web service's environment:. Give numeric variables their documented default instead of an empty one, e.g. LOGIN_MAX_ATTEMPTS: ${LOGIN_MAX_ATTEMPTS:-5}: an empty value is read as 0 (with LOGIN_MAX_ATTEMPTS that refuses every portal login). After changing .env, recreate the container with docker compose up -d; docker compose restart keeps the old values.

Table of Contents

  1. Required Security Variables
  2. Application Configuration
  3. OAuth2/OIDC Authentication
  4. Forward Auth
  5. Rate Limiting
  6. Instance Sync
  7. ClickHouse Analytics
  8. Rootless Docker Operation
  9. Internal/Build Variables
  10. Database-Persisted Settings
  11. Environment Variable Checklist
  12. Troubleshooting

Required Security Variables

The app won't start in production without these three variables set correctly.

SESSION_SECRET

Encryption key for session cookies and JWT tokens, and for the secrets CPM stores in its database. Required in production, minimum 32 characters.

In production, the app checks that this is at least 32 characters and not a known placeholder: change-me-in-production, the built-in development secret, or your-secure-session-secret-here-min-32-chars (the value older .env.example files shipped with). Each deployment needs its own unique secret. A placeholder stops the web container with:

SESSION_SECRET is using a known insecure placeholder value. Generate a secure secret with: openssl rand -base64 32. ...

.env.example leaves SESSION_SECRET empty, and docker compose refuses to start while it is empty (ERROR - SESSION_SECRET is required).

Generate one:

openssl rand -base64 32

Example:

SESSION_SECRET="<output of openssl rand -base64 32>"

Important: Changing this logs out all users immediately. Never commit it to version control, and back it up together with the database.

Warning: Values stored in the database (DNS provider credentials, OAuth client secrets and sign-in tokens, instance API tokens, the instance sync master token, imported certificate and CA private keys) are encrypted with a key derived from this secret. To change SESSION_SECRET without re-entering them, put the old value in SESSION_SECRET_PREVIOUS when you change it; without it, those stored values can no longer be decrypted and have to be re-entered. See Security Configuration#secret-rotation and Troubleshooting#secret-decryption-issues.

Upgrading from a placeholder (since v1.13.1, which rejects the old .env.example value): just set a new secret. Values stored under a placeholder are re-encrypted with the new secret on the next start, with nothing to re-enter.

Instance sync: the master and each slave can use their own SESSION_SECRET (since v1.13.1). A slave derives the sync key that synced secrets are sealed to from it, and the master pins that key, so rotate a slave's secret with SESSION_SECRET_PREVIOUS as described in Feature Guide Instance Sync#rotating-a-slaves-session_secret. A slave still on v1.12.0 or earlier needs its master's secret.

Development mode uses a default value (which is obviously insecure).


SESSION_SECRET_PREVIOUS

Description: Earlier SESSION_SECRET values, comma-separated. They are only used to decrypt stored secrets after a rotation and, on an instance sync slave, to prove its new sync key to the master; nothing is ever encrypted with them. (Since v1.13.1.)

  • Default: empty
  • Passed to the web container by docker-compose.yml

Rotating SESSION_SECRET:

  1. Set SESSION_SECRET to the new value and SESSION_SECRET_PREVIOUS to the old one.
  2. Recreate the web container: docker compose up -d. On startup every stored secret that only an old key decrypts is re-encrypted with the new SESSION_SECRET, logged as Re-encrypted N stored secret(s) with the current SESSION_SECRET.
  3. Remove SESSION_SECRET_PREVIOUS after one successful start.

Notes:

  • A stored value that no key decrypts is left as stored and logged as [secret] … cannot be decrypted with SESSION_SECRET or SESSION_SECRET_PREVIOUS, followed by a count. OAuth sign-in tokens that no key decrypts are cleared instead (the next OAuth sign-in stores new ones). See Troubleshooting#secret-decryption-issues.
  • The whole value is also tried as a single secret, so an old secret that contains a comma still works.
  • On an instance sync slave, keep the old value until the master has synced to it once since the restart: the master has pinned the slave's old sync key and accepts the new one only when the slave proves it with the old secret. See Feature Guide Instance Sync#rotating-a-slaves-session_secret.
  • While a slave's master runs v1.12.0 or earlier, the slave needs the master's secret as SESSION_SECRET or in SESSION_SECRET_PREVIOUS, because that master sends DNS provider credentials encrypted with its own secret.

Source: src/lib/config.ts, src/lib/secret.ts, src/lib/secret-rotation.ts


LEGACY_KEY_CUTOFF_DATE

Description: Controls how long secrets encrypted with the legacy key derivation (plain SHA-256 of SESSION_SECRET, used before HKDF was introduced) can still be decrypted.

  • Default: 2026-06-01T00:00:00Z
  • Accepts an ISO 8601 date, or never to disable the cutoff entirely (temporary measure only)

After the cutoff, decryption falls back to the HKDF-derived key only; legacy-format secrets throw a "legacy key grace period has expired" error until they are re-entered and re-encrypted with the current key.

Note: This only affects secrets whose stored format is outdated. If SESSION_SECRET itself changed, both the current and legacy keys fail — set SESSION_SECRET_PREVIOUS to the old secret (its legacy key is tried too during the grace period) or re-enter the affected tokens instead. See Troubleshooting#secret-decryption-issues.

Source: src/lib/secret.ts


ADMIN_USERNAME

Username for administrator login. Required.

Use 3–255 characters from A-Z a-z 0-9 _ . @ -; the sign-in page refuses any other username and ignores case. The default is "admin" which works fine (only in development mode does it allow the insecure "admin" default).

Example:

ADMIN_USERNAME="admin"

This is the primary admin account (user id 1). Further users and roles are managed on the Users page; see Feature Guide User Management.


ADMIN_PASSWORD

Password for administrator login. Required, 12+ characters in production.

Production enforces:

  • At least 12 characters
  • Uppercase (A-Z), lowercase (a-z), numbers (0-9), and special characters (!@#$%^&*)
  • Can't be the default "admin"
  • Can't be an example password from the documentation, such as the value older .env.example files shipped with (since v1.13.1)

Example:

ADMIN_PASSWORD="<choose-your-own: 12+ chars, upper, lower, digit, symbol>"

The app checks this at startup and refuses to start if it doesn't meet requirements (Admin credentials validation failed: followed by the reasons, e.g. ADMIN_PASSWORD is an example value from the documentation; choose your own password). .env.example leaves it empty, and docker compose refuses to start while it is empty (ERROR - ADMIN_PASSWORD is required). The password is hashed with bcrypt before storing in the database.

You can change it later from the profile page; since v1.13.1 that change survives restarts (see below).


How ADMIN_USERNAME and ADMIN_PASSWORD Are Applied

(Since v1.13.1. Older releases re-applied both on every start, so a password changed in the UI reverted at the next restart.)

The environment credentials are applied when the primary admin account is created and whenever ADMIN_USERNAME or ADMIN_PASSWORD changes. CPM remembers the applied username and a bcrypt hash of the applied password to detect a change.

  • Password changed in the UI: kept across restarts. ADMIN_PASSWORD no longer signs in until you change it.

  • Recovering a lost admin password: set ADMIN_PASSWORD (or ADMIN_USERNAME) to a new value and recreate the web container with docker compose up -d (docker compose restart keeps the old values). This resets the primary admin's password and its username to ADMIN_USERNAME (email <ADMIN_USERNAME>@localhost), restores its admin role, re-activates it if it was disabled and, when the password changed, signs out all of its dashboard and forward-auth sessions.

  • ADMIN_USERNAME used by another account (since v1.13.1): a new ADMIN_USERNAME that another account already signs in with, or has as its email address (also as <ADMIN_USERNAME>@localhost), is not applied. Nothing changes on that start, and the log shows, after Failed to initialize database::

    ADMIN_USERNAME "…" is not applied: another account already signs in with it or has it as its email address. Give that account a different username or email address on the Users page, or choose another ADMIN_USERNAME.
    

    Every start tries again until that account's username or email address changes or you choose another ADMIN_USERNAME.

    This also applies when you change only ADMIN_PASSWORD after the primary admin's username or email was changed on the Users page: applying the environment credentials resets them to ADMIN_USERNAME, so if another account holds that name (or <ADMIN_USERNAME>@localhost) the password is not reset either and the same error is logged.

  • First start after upgrading from v1.12.0 or earlier: if the stored admin password is ADMIN_PASSWORD, admin or a documented example, ADMIN_PASSWORD is applied. Any other stored password was probably changed in the UI and is kept, with the warning:

    ADMIN_PASSWORD differs from the stored admin password; keeping the stored password because it was probably changed in the UI. Change ADMIN_PASSWORD again and recreate the web container (docker compose up -d) to force it.
    

    A changed ADMIN_USERNAME is still applied on that start, and re-applying unchanged credentials does not re-activate a disabled primary admin.

Source: src/lib/init-db.ts, src/lib/config.ts


Application Configuration

BASE_URL

Description: Public-facing base URL of the application for external access.

Property Value
Type String (URL)
Required No
Default http://localhost:3000
Environment All

Usage:

  • OAuth redirect URIs
  • Email links (future feature)
  • CORS configuration
  • NextAuth URL configuration

Example:

BASE_URL="https://proxy.example.com"
BASE_URL="http://192.168.1.100:3000"

Validation: None

Related Variables:

  • OAUTH_AUTHORIZATION_URL - OAuth redirects use this base
  • Used by Better Auth as the canonical base URL for callbacks

Notes:

  • Include protocol (http:// or https://)
  • No trailing slash
  • Must match external access URL for OAuth to work correctly
  • Used for generating absolute URLs in the application

Source: src/lib/config.ts:154, docker-compose.yml:30, docker-compose.yml:37


CADDY_API_URL

Description: Caddy Admin API endpoint for configuration management.

Property Value
Type String (URL)
Required No
Default Production: http://caddy:2019
Development: http://localhost:2019
Environment All

Usage:

  • Caddy configuration updates
  • Certificate management
  • Health checks
  • Proxy host management

Example:

# Docker Compose (default - uses service name)
CADDY_API_URL="http://caddy:2019"

# Custom Caddy instance
CADDY_API_URL="http://192.168.1.50:2019"

# Different port
CADDY_API_URL="http://caddy:3000"

Validation: None

Related Variables: None

Notes:

  • Port 2019 is Caddy's admin API (not public HTTP)
  • Should NOT be exposed to public internet
  • Must be accessible from web container
  • Used for JSON API calls to configure Caddy
  • Health check: GET /config/

Source: src/lib/config.ts:8, src/lib/config.ts:153, docker-compose.yml:27


DATABASE_URL

Description: SQLite database connection string.

Property Value
Type String (file:// URL)
Required No
Default file:/app/data/caddy-proxy-manager.db
Environment All

Usage:

  • Drizzle ORM connection
  • Database initialization
  • Migrations

Example:

DATABASE_URL="file:/app/data/caddy-proxy-manager.db"
DATABASE_URL="file:/custom/path/database.db"
DATABASE_URL="file:./data/caddy-proxy-manager.db"  # Relative path

Validation: None

Related Variables: DATABASE_PATH

Notes:

  • Only SQLite supported currently
  • File path must be writable
  • Docker volume recommended for persistence
  • Format: file: prefix followed by path
  • Can use relative or absolute paths
  • When the database is opened, the database file and its -journal/-wal/-shm files lose their world permission bits; owner and group bits are left unchanged (since v1.13.1). See Rootless Docker Operation.

Source: src/lib/db.ts, docker-compose.yml:34, drizzle.config.ts


DATABASE_PATH

Description: File system path to SQLite database (alternative to DATABASE_URL).

Property Value
Type String (file path)
Required No
Default /app/data/caddy-proxy-manager.db (Docker)
Environment All

Usage:

  • Fallback if DATABASE_URL not set
  • Direct file path specification
  • Database file location

Example:

DATABASE_PATH="/app/data/caddy-proxy-manager.db"
DATABASE_PATH="/custom/data/db.sqlite"

Validation: None

Related Variables: DATABASE_URL (takes precedence if both set)

Notes:

  • DATABASE_URL takes precedence if both are set
  • Must be absolute path in Docker containers
  • Directory must exist and be writable
  • Used in entrypoint scripts and DB initialization

Source: docker-compose.yml:33, docker/web/entrypoint.sh


CERTS_DIRECTORY

Description: Directory for storing custom certificate files.

Property Value
Type String (directory path)
Required No
Default ./data/certs (relative to working directory)
Environment All

Usage:

  • Custom certificate imports
  • Certificate file storage

Example:

CERTS_DIRECTORY="/app/data/certs"
CERTS_DIRECTORY="/custom/cert/path"

Validation: None

Related Variables: None

Notes:

  • Directory must be writable
  • Private keys are not written here: imported keys are stored encrypted in the database (see Security Configuration#certificate-security) and passed to Caddy inline
  • Separate from Caddy's automatic ACME certificates
  • Permissions set to 0o700 (owner read/write/execute only)
  • Created automatically if doesn't exist

Source: src/lib/caddy.ts


OAuth2/OIDC Authentication

OAuth2/OIDC enables single sign-on with identity providers like Authentik, Keycloak, Auth0, etc.

OAUTH_ENABLED

Description: Enable OAuth2/OIDC authentication.

Property Value
Type Boolean
Required No
Default false
Environment All

Example:

OAUTH_ENABLED=true
OAUTH_ENABLED=false

Validation: Must be "true" or "false" (case-sensitive)

Related Variables: All OAUTH_* variables

Notes:

  • OAuth login appears alongside credentials when enabled
  • Requires all OAuth credentials to be configured
  • Does not disable password authentication
  • Setting to "true" requires OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_ISSUER

Source: src/lib/config.ts:162, docker-compose.yml:46


OAUTH_PROVIDER_NAME

Description: Display name for OAuth provider on login page.

Property Value
Type String
Required No
Default OAuth2
Environment All

Example:

OAUTH_PROVIDER_NAME="Authentik"
OAUTH_PROVIDER_NAME="Keycloak"
OAUTH_PROVIDER_NAME="Company SSO"

Validation: None

Related Variables: OAUTH_ENABLED

Notes:

  • Cosmetic only, shown on login button ("Sign in with {name}")
  • Use recognizable name for your users
  • Doesn't affect OAuth functionality

Source: src/lib/config.ts:163, docker-compose.yml:47


OAUTH_CLIENT_ID

Description: OAuth2 client ID from your identity provider.

Property Value
Type String
Required If OAUTH_ENABLED=true
Default None
Environment All

Example:

OAUTH_CLIENT_ID="caddy-proxy-manager"
OAUTH_CLIENT_ID="1234567890abcdef"

Validation: Required if OAuth enabled

Related Variables: OAUTH_CLIENT_SECRET, OAUTH_ENABLED

Notes:

  • Obtain from OAuth provider configuration
  • Not sensitive (can be public in some OAuth flows)
  • Used in authorization requests

Source: src/lib/config.ts:164, docker-compose.yml:48


OAUTH_CLIENT_SECRET

Description: OAuth2 client secret from your identity provider.

Property Value
Type String (secret)
Required If OAUTH_ENABLED=true
Default None
Environment All

Example:

OAUTH_CLIENT_SECRET="your-client-secret"

Validation: Required if OAuth enabled

Related Variables: OAUTH_CLIENT_ID, OAUTH_ENABLED

Notes:

  • SENSITIVE - keep secure, never commit to version control
  • Obtain from OAuth provider
  • Used in token exchange

Source: src/lib/config.ts:165, docker-compose.yml:49


OAUTH_ISSUER

Description: OIDC issuer URL for provider discovery.

Property Value
Type String (URL)
Required If OAUTH_ENABLED=true
Default None
Environment All

Usage:

  • OIDC discovery (auto-detects endpoints via /.well-known/openid-configuration)
  • Token validation
  • Provider verification

Example:

# Authentik
OAUTH_ISSUER="https://auth.example.com/application/o/caddy-proxy/"

# Keycloak
OAUTH_ISSUER="https://keycloak.example.com/realms/myrealm"

# Auth0
OAUTH_ISSUER="https://yourtenant.auth0.com/"

Validation: Required if OAuth enabled

Related Variables:

  • OAUTH_AUTHORIZATION_URL (auto-discovered from issuer)
  • OAUTH_TOKEN_URL (auto-discovered from issuer)
  • OAUTH_USERINFO_URL (auto-discovered from issuer)

Notes:

  • Must support OIDC discovery endpoint
  • Include trailing slash if required by provider
  • Auto-discovery preferred over manual URL configuration
  • Discovery URL: {OAUTH_ISSUER}/.well-known/openid-configuration

Source: src/lib/config.ts:166, docker-compose.yml:50


OAUTH_AUTHORIZATION_URL

Description: Manual override for OAuth authorization endpoint.

Property Value
Type String (URL)
Required No (auto-discovered from issuer)
Default None (auto-discovered)
Environment All

Usage:

  • Override auto-discovery
  • Non-OIDC OAuth providers

Example:

OAUTH_AUTHORIZATION_URL="https://auth.example.com/oauth/authorize"

Validation: None

Related Variables: OAUTH_ISSUER

Notes:

  • Only needed if OIDC discovery fails
  • Prefer using OAUTH_ISSUER for auto-discovery
  • Rarely needed with modern OIDC providers

Source: src/lib/config.ts:167, docker-compose.yml:51


OAUTH_TOKEN_URL

Description: Manual override for OAuth token endpoint.

Property Value
Type String (URL)
Required No (auto-discovered from issuer)
Default None (auto-discovered)
Environment All

Usage:

  • Override auto-discovery
  • Non-OIDC OAuth providers

Example:

OAUTH_TOKEN_URL="https://auth.example.com/oauth/token"

Validation: None

Related Variables: OAUTH_ISSUER

Notes:

  • Only needed if OIDC discovery fails
  • Prefer using OAUTH_ISSUER for auto-discovery

Source: src/lib/config.ts:168, docker-compose.yml:52


OAUTH_USERINFO_URL

Description: Manual override for OAuth userinfo endpoint.

Property Value
Type String (URL)
Required No (auto-discovered from issuer)
Default None (auto-discovered)
Environment All

Usage:

  • Override auto-discovery
  • Fetch user profile information

Example:

OAUTH_USERINFO_URL="https://auth.example.com/oauth/userinfo"

Validation: None

Related Variables: OAUTH_ISSUER

Notes:

  • Only needed if OIDC discovery fails
  • Prefer using OAUTH_ISSUER for auto-discovery

Source: src/lib/config.ts:169, docker-compose.yml:53


OAUTH_ALLOW_AUTO_LINKING

Description: Automatically link OAuth accounts to existing users without passwords.

Property Value
Type Boolean
Required No
Default false
Environment All

Usage:

  • Automatic account linking
  • User convenience

Example:

OAUTH_ALLOW_AUTO_LINKING=true
OAUTH_ALLOW_AUTO_LINKING=false

Validation: Must be "true" or "false"

Related Variables: OAUTH_ENABLED

Notes:

  • Security consideration: enables automatic linking without confirmation
  • Users can manually link from profile page regardless of this setting
  • Only links accounts without existing passwords
  • Set to false for more secure manual linking

Source: src/lib/config.ts, docker-compose.yml


AUTH_ALLOW_OAUTH_REGISTRATION

Description: Allow a first-time OAuth/OIDC identity to create a new Caddy Proxy Manager user.

Property Value
Type Boolean
Required No
Default false
Environment All

Example:

# OIDC-only self-registration
AUTH_ALLOW_SELF_REGISTRATION=false
AUTH_ALLOW_OAUTH_REGISTRATION=true

Validation: Must be "true" or "false" (case-sensitive)

Related Variables: OAUTH_ENABLED, AUTH_ALLOW_SELF_REGISTRATION, OAUTH_ALLOW_AUTO_LINKING

Notes:

  • This flag is independent of email/password self-registration.
  • When false, existing users and configured account-linking flows still work, but unknown OAuth identities cannot be auto-provisioned.
  • Keep AUTH_ALLOW_SELF_REGISTRATION=false to permit onboarding only through OAuth/OIDC.
  • OAuth-created users receive the safe default user role unless trusted role claims are explicitly enabled separately.

Source: src/lib/config.ts, src/lib/auth-server.ts, docker-compose.yml


Forward Auth

Variables for the built-in forward auth portal. See Feature Guide Forward Auth.

FORWARD_AUTH_ALLOWED_PORTS

Description: Non-default external ports on which browsers reach forward-auth protected sites. (Since v1.13.1.)

Property Value
Type String (comma-separated port numbers)
Required Only if protected sites are served on a port other than 80 (http) or 443 (https)
Default Empty
Environment Runtime

Example:

# Caddy published as "8443:443"
FORWARD_AUTH_ALLOWED_PORTS=8443

# Several ports
FORWARD_AUTH_ALLOWED_PORTS=8443,9443

Validation: Entries that are not a port number from 1 to 65535 are ignored

Related Variables: TRUSTED_CLIENT_IP_HEADER

Notes:

  • Caddy matches proxy hosts by hostname only, so CPM refuses portal logins, redirects and sessions for a URL on a non-default port unless that port is listed here
  • Leave it empty when Caddy is published on the standard 80/443
  • For an unlisted port the portal shows "This site is served on port 8443, which is not allowed for forward authentication. Ask the administrator to add it to FORWARD_AUTH_ALLOWED_PORTS." and the web container logs [forward-auth] Rejected <host>:8443 because port 8443 is not listed in FORWARD_AUTH_ALLOWED_PORTS. … (once per port per hour)
  • After a change, recreate the web container: docker compose up -d
  • Passed to the web container by docker-compose.yml

Source: src/lib/config.ts, src/lib/models/forward-auth.ts, docker-compose.yml


Rate Limiting

Dashboard sign-in is limited by Better Auth: /api/auth/sign-in/* and /api/auth/sign-up/* keep its built-in limit of 3 requests per 10 seconds per client address, which only AUTH_RATE_LIMIT_ENABLED=false turns off. AUTH_RATE_LIMIT_WINDOW and AUTH_RATE_LIMIT_MAX apply to Better Auth's other /api/auth endpoints. The LOGIN_* variables control the forward-auth portal login and the password change and account-linking endpoints. The stock docker-compose.yml passes neither the AUTH_RATE_LIMIT_* nor the LOGIN_* variables through; add them to the web service's environment: to change them.

AUTH_RATE_LIMIT_ENABLED

Description: Enable or disable Better Auth's built-in rate limiting for all /api/auth endpoints, dashboard sign-in included.

Property Value
Type Boolean string
Required No
Default true (enabled unless set to "false")
Environment All

Example:

AUTH_RATE_LIMIT_ENABLED=true
AUTH_RATE_LIMIT_ENABLED=false  # Not recommended

AUTH_RATE_LIMIT_WINDOW

Description: Time window (in seconds) for the rate limit counter of Better Auth's /api/auth endpoints. Sign-in and sign-up keep Better Auth's built-in window of 10 seconds.

Property Value
Type Number (seconds)
Required No
Default 60
Environment All

Example:

AUTH_RATE_LIMIT_WINDOW=60    # 1 minute
AUTH_RATE_LIMIT_WINDOW=120   # 2 minutes

Related Variables: AUTH_RATE_LIMIT_MAX


AUTH_RATE_LIMIT_MAX

Description: Maximum number of auth requests allowed per window before rate limiting kicks in.

Property Value
Type Number
Required No
Default 5
Environment All

Example:

AUTH_RATE_LIMIT_MAX=5
AUTH_RATE_LIMIT_MAX=10   # More lenient
AUTH_RATE_LIMIT_MAX=3    # More strict

Related Variables: AUTH_RATE_LIMIT_WINDOW

Notes:

  • In-memory tracking (not suitable for multi-instance deployments)
  • Applies to Better Auth's /api/auth endpoints except sign-in and sign-up, which keep 3 requests per 10 seconds
  • After the window expires, the counter resets

Source: src/lib/auth-server.ts


AUTH_TRUST_HOST

Description: Trust the incoming Host header for URL construction (callback URLs, etc.). Only enable behind reverse proxies that rewrite the Host header without setting X-Forwarded-Host.

Property Value
Type Boolean string
Required No
Default false
Environment All

Example:

AUTH_TRUST_HOST=true   # Only if behind a proxy that rewrites Host

Notes:

  • When false (default), Better Auth uses BASE_URL to construct callback URLs — this is the secure default
  • Only set to true if your reverse proxy rewrites the Host header and does not set X-Forwarded-Host
  • Setting to true in an exposed environment could allow Host header poisoning

LOGIN_* Variables (Portal Login, Password Change and Account Linking)

The following variables are used by the forward-auth portal login (/api/forward-auth/login), the password change endpoint (/api/user/change-password) and account linking:

Variable Description Default
LOGIN_MAX_ATTEMPTS Max failed attempts per window 5
LOGIN_WINDOW_MS Time window in milliseconds 300000 (5 min)
LOGIN_BLOCK_MS Block duration in milliseconds 900000 (15 min)

Portal login limits (since v1.13.1):

  • LOGIN_MAX_ATTEMPTS failures from one client, or from one client against one account, within LOGIN_WINDOW_MS block that client (for that account) for LOGIN_BLOCK_MS. IPv6 clients are counted per /64 prefix.
  • Failures against one account from all clients combined are counted over one hour (or LOGIN_WINDOW_MS if longer). Reaching the ceiling, LOGIN_MAX_ATTEMPTS × (⌈window ÷ min(LOGIN_WINDOW_MS, LOGIN_BLOCK_MS)⌉ + 1) and at least 10 × LOGIN_MAX_ATTEMPTS (65 with the defaults), blocks the account for LOGIN_BLOCK_MS. A successful login clears the client's own counters but not this one.
  • Attempts still being checked count towards every limit; blocked or extra concurrent attempts get HTTP 429 "Too many login attempts. Please try again later."
  • The client address comes from TRUSTED_CLIENT_IP_HEADER or the rightmost X-Forwarded-For entry; a client-sent X-Real-IP is no longer trusted.

See Feature Guide Forward Auth#portal-login-rate-limits for details.


TRUSTED_CLIENT_IP_HEADER

Description: Request header holding the real client IP for the per-IP rate limits of the forward-auth portal login and the instance sync endpoint. (Since v1.13.1.)

Property Value
Type String (header name, e.g. cf-connecting-ip or x-real-ip)
Required No
Default Unset: the rightmost X-Forwarded-For entry is used
Environment Runtime

Example:

# Cloudflare in front of Caddy, and the origin only accepts connections from Cloudflare
TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip

Validation: Must be a valid header name; an invalid one is ignored and logged once as TRUSTED_CLIENT_IP_HEADER is not a valid header name: …

Related Variables: LOGIN_MAX_ATTEMPTS, INSTANCE_SYNC_RATE_MAX

Notes:

  • Leave it unset when Caddy (including a CPM proxy host) is the outermost proxy in front of CPM: Caddy passes X-Real-IP and CF-Connecting-IP through unchanged, and the rightmost X-Forwarded-For entry is then the real client
  • Behind a CDN the rightmost X-Forwarded-For entry is the CDN edge, so set the CDN's header, but only if the origin accepts connections from the CDN alone
  • Security: set it only if every route to CPM sets or overwrites the header; otherwise clients can forge it and pick their own rate-limit key
  • A request without a usable address in the header falls back to X-Forwarded-For
  • When port 3000 is reached directly, clients control X-Forwarded-For and the per-IP limits are only best effort; expose the portal (BASE_URL) through Caddy or another proxy that overwrites X-Forwarded-For
  • Passed to the web container by docker-compose.yml

Source: src/lib/client-ip.ts, docker-compose.yml


Instance Sync

Instance Sync allows you to run multiple Caddy Proxy Manager instances in a master/slave configuration for high availability or distributed deployments. Configuration can be set via environment variables for infrastructure-as-code workflows.

Of these variables, the stock docker-compose.yml passes only INSTANCE_SYNC_TIMEOUT_MS to the web container; add the others under the web service's environment: (as in the Docker Compose example below).

INSTANCE_MODE

Description: Set the instance mode for multi-instance deployments.

Property Value
Type String
Required No
Default standalone
Values standalone, master, slave
Environment Runtime

Usage:

  • Configure instance role at startup
  • Infrastructure-as-code deployments
  • Prevent runtime changes to instance mode

Example:

# Master instance (pushes configuration to slaves)
INSTANCE_MODE=master

# Slave instance (receives configuration from master)
INSTANCE_MODE=slave

# Standalone (default, independent operation)
INSTANCE_MODE=standalone

Validation: Must be one of: standalone, master, slave

Related Variables: INSTANCE_SYNC_TOKEN

Notes:

  • Environment variable takes precedence over database setting
  • When set via environment, the instance mode cannot be changed from the UI
  • Master instances push proxy hosts, certificates, access lists, and settings to slaves
  • Slave instances receive configuration from master and apply it locally

Source: src/lib/instance-sync.ts


INSTANCE_SYNC_TOKEN

Description: Sync token for authentication between master and slave instances.

Property Value
Type String (secret)
Required If INSTANCE_MODE=slave
Default None
Environment Runtime

Usage:

  • Authenticate sync requests from master
  • Secure inter-instance communication

Example:

# Generate a secure token (minimum 32 characters)
openssl rand -base64 32

# Set on slave instance
INSTANCE_SYNC_TOKEN="your-secure-32-character-minimum-token"

Validation: 32–512 characters with no leading or trailing whitespace, whether set via the UI or the environment. With INSTANCE_MODE=slave set in the environment, a missing or invalid INSTANCE_SYNC_TOKEN stops the web container at startup (INSTANCE_SYNC_TOKEN for slave mode is invalid: …)

Related Variables: INSTANCE_MODE

Notes:

  • SENSITIVE - keep secure, never commit to version control
  • Environment variable takes precedence over database setting
  • When set via environment, the token cannot be changed from the UI
  • Must match the API token configured on the master for this slave
  • Used for Bearer token authentication on /api/instances/sync endpoint
  • Tokens stored in the database are encrypted at rest using SESSION_SECRET

Source: src/lib/instance-sync.ts


INSTANCE_SLAVES

Description: Configure slave instances for the master to sync to via environment variable.

Property Value
Type String (JSON array)
Required No
Default None
Environment Runtime

Usage:

  • Configure slave instances at startup without using the UI
  • Infrastructure-as-code deployments
  • Docker Compose or Kubernetes configurations

Example:

# Single slave
INSTANCE_SLAVES='[{"name":"slave1","url":"http://slave:3000","token":"your-32-char-token-here"}]'

# Multiple slaves
INSTANCE_SLAVES='[{"name":"slave1","url":"http://slave1:3000","token":"your-sync-token-32chars-min"},{"name":"slave2","url":"http://slave2:3000","token":"your-sync-token-32chars-min"}]'

# Slave with its sync key pinned (full public key from the slave's Settings → Instance Sync page)
INSTANCE_SLAVES='[{"name":"replica","url":"https://replica.example.com","token":"<sync-token>","syncPublicKey":"<slave sync public key>"}]'

Validation: Must be valid JSON array of objects with name, url, and token fields, plus the optional syncPublicKey or syncKeyId. Invalid entries are skipped:

  • A url must pass the same checks as instances added in the UI: http(s) only, no credentials, query string or fragment. Otherwise the master logs Skipping INSTANCE_SLAVES entry <index>: <reason>, e.g. Base URL must not contain a query string or fragment (since v1.13.1)
  • syncKeyId must be 16 lowercase hex characters and syncPublicKey base64 of 32 bytes; when both are set they must match. Otherwise the entry is skipped with Skipping INSTANCE_SLAVES entry <index>: syncKeyId must be a sync key id (16 lowercase hex characters), … syncPublicKey must be a sync public key (base64 of 32 bytes) or … syncKeyId is not the key id of syncPublicKey

Related Variables: INSTANCE_MODE, INSTANCE_SYNC_INTERVAL, INSTANCE_SYNC_TIMEOUT_MS

Notes:

  • Only used when INSTANCE_MODE=master
  • Each object requires: name (display name), url (slave base URL), token (sync token matching slave's INSTANCE_SYNC_TOKEN)
  • Optional key pin (since v1.13.1): syncPublicKey (the slave's full sync public key, compared byte for byte; recommended) or syncKeyId (its 16-hex-character key id, a 64-bit fingerprint). With either, the master syncs only to that key, with no first-use pin and no automatic re-pin, so update the entry after rotating the slave's SESSION_SECRET. Another key fails the sync with "Slave sync key does not match the key configured in INSTANCE_SLAVES". Without them, the master pins the slave's key on first use. See Feature Guide Instance Sync#sync-key-pinning
  • Environment-configured slaves are synced alongside UI-configured slaves
  • Sync results for env-configured slaves are logged to console (not stored in database)

Source: src/lib/instance-sync.ts


INSTANCE_SYNC_INTERVAL

Description: Enable periodic background sync from master to slaves.

Property Value
Type Number (seconds)
Required No
Default 0 (disabled)
Environment Runtime

Usage:

  • Automatic periodic sync as fallback if push sync fails
  • Ensure slaves stay in sync even after temporary network issues

Example:

# Sync every 60 seconds
INSTANCE_SYNC_INTERVAL=60

# Sync every 5 minutes
INSTANCE_SYNC_INTERVAL=300

# Disabled (default, relies on push-based sync only)
INSTANCE_SYNC_INTERVAL=0

Validation: Minimum 30 seconds if enabled (values below 30 are raised to 30)

Related Variables: INSTANCE_MODE, INSTANCE_SLAVES

Notes:

  • Only active when INSTANCE_MODE=master
  • Push-based sync still happens on every configuration change
  • Periodic sync is a safety net for missed updates
  • Set to 0 to disable and rely only on push-based sync

Source: src/lib/instance-sync.ts, src/instrumentation.ts


INSTANCE_SYNC_ALLOW_HTTP

Description: Allow sync over HTTP (insecure). Required when slaves use HTTP URLs.

Property Value
Type Boolean
Required No
Default false
Environment Runtime

Usage:

  • Allow sync to slaves over HTTP instead of HTTPS
  • Required for internal Docker networks without TLS
  • Security Warning: Tokens are transmitted in plaintext over HTTP

Example:

# Allow HTTP sync (use only in trusted networks!)
INSTANCE_SYNC_ALLOW_HTTP=true

# Block HTTP sync (default, recommended for production)
INSTANCE_SYNC_ALLOW_HTTP=false

Validation: Must be "true" or "1" to enable

Related Variables: INSTANCE_SLAVES, INSTANCE_MODE

Notes:

  • SECURITY WARNING: HTTP sync transmits authentication tokens in plaintext
  • Only use in trusted networks (e.g., internal Docker networks, private VPCs)
  • For production, always use HTTPS URLs for slave instances
  • If not set and HTTP URLs are detected, sync will be blocked with an error message

Source: src/lib/instance-sync.ts


INSTANCE_SYNC_TIMEOUT_MS

Description: Time limit for one sync request from the master to a slave, covering the upload and the slave's apply, including its Caddy reload. (Since v1.13.1.)

Property Value
Type Number (milliseconds)
Required No
Default 60000 (60 seconds)
Environment Runtime (master)

Example:

# Slow slaves with large configurations
INSTANCE_SYNC_TIMEOUT_MS=120000

Validation: Clamped to 5000–300000; 0, an empty or a non-numeric value means the default

Related Variables: INSTANCE_SLAVES, INSTANCE_SYNC_INTERVAL

Notes:

  • Only used on the master
  • A request that exceeds the limit is reported as "Sync timed out"; the slave may still finish applying the configuration
  • Periodic syncs never overlap: while one is still running, the next is skipped and logged as Periodic sync skipped: the previous sync is still running
  • Passed to the web container by docker-compose.yml (default 60000)

Source: src/lib/instance-sync.ts, docker-compose.yml


INSTANCE_SYNC_MAX_BYTES

Description: Maximum allowed sync payload size (bytes).

Property Value
Type Number (bytes)
Required No
Default 10485760 (10 MB)
Environment Runtime

Usage:

  • Protect slave instances from oversized payloads
  • Helps prevent accidental or malicious large sync requests

Example:

# 10 MB (default)
INSTANCE_SYNC_MAX_BYTES=10485760

# 5 MB
INSTANCE_SYNC_MAX_BYTES=5242880

Validation: Must be a positive integer

Related Variables: INSTANCE_MODE

Notes:

  • Applies to /api/instances/sync on slave instances
  • Payloads larger than the limit return HTTP 413

Source: app/api/instances/sync/route.ts


INSTANCE_SYNC_RATE_MAX

Description: Slave only: requests per client address and window to /api/instances/sync, counted separately for syncs (POST) and key requests (GET). The client address comes from TRUSTED_CLIENT_IP_HEADER or the rightmost X-Forwarded-For entry. (Separate counting and the client address source are new since v1.13.1.)

Property Value
Type Number
Required No
Default 60
Environment Runtime

Usage:

  • Throttle sync requests to protect slave instances

Example:

# Allow up to 60 sync requests per window (default)
INSTANCE_SYNC_RATE_MAX=60

# More restrictive
INSTANCE_SYNC_RATE_MAX=10

Validation: Must be a positive integer

Related Variables: INSTANCE_SYNC_RATE_WINDOW_MS

Notes:

  • Applies to /api/instances/sync on slave instances
  • Every request counts, before authentication; the window is fixed, so after the limit requests are refused until it ends
  • Returns HTTP 429 when exceeded

Source: app/api/instances/sync/route.ts


INSTANCE_SYNC_RATE_WINDOW_MS

Description: Time window in milliseconds for sync rate limiting.

Property Value
Type Number (milliseconds)
Required No
Default 60000 (60 seconds)
Environment Runtime

Usage:

  • Controls the rate-limit window for sync requests

Example:

# 60 seconds (default)
INSTANCE_SYNC_RATE_WINDOW_MS=60000

# 5 minutes
INSTANCE_SYNC_RATE_WINDOW_MS=300000

Validation: Must be a positive integer

Related Variables: INSTANCE_SYNC_RATE_MAX

Notes:

  • Applies to /api/instances/sync on slave instances

Source: app/api/instances/sync/route.ts


Instance Sync Configuration Examples

Master Instance

# Master pushes configuration to slaves
INSTANCE_MODE=master

# Option 1: Configure slaves via environment variable
INSTANCE_SLAVES='[{"name":"slave1","url":"https://slave.example.com","token":"your-sync-token-32chars-min"}]'

# Option 2: Configure slaves via UI (no env var needed)
# Slave instances can be added in Settings → Instance Sync

# Optional: Enable periodic sync every 60 seconds
INSTANCE_SYNC_INTERVAL=60

# Only if using HTTP URLs (not recommended for production!)
# INSTANCE_SYNC_ALLOW_HTTP=true

Slave Instance

# Slave receives configuration from master
INSTANCE_MODE=slave
INSTANCE_SYNC_TOKEN="your-secure-32-character-minimum-token"

Docker Compose Multi-Instance Setup

services:
  web-master:
    image: caddy-proxy-manager
    environment:
      INSTANCE_MODE: master
      # Configure slaves via environment variable (matching slave tokens)
      INSTANCE_SLAVES: '[{"name":"slave1","url":"http://web-slave-1:3000","token":"your-secure-32-character-minimum-token"},{"name":"slave2","url":"http://web-slave-2:3000","token":"your-secure-32-character-minimum-token"}]'
      # Optional: periodic sync every 60 seconds
      INSTANCE_SYNC_INTERVAL: "60"
      # Required for HTTP URLs (internal Docker network)
      INSTANCE_SYNC_ALLOW_HTTP: "true"
      # ... other env vars

  web-slave-1:
    image: caddy-proxy-manager
    environment:
      INSTANCE_MODE: slave
      INSTANCE_SYNC_TOKEN: "your-secure-32-character-minimum-token"
      # ... other env vars

  web-slave-2:
    image: caddy-proxy-manager
    environment:
      INSTANCE_MODE: slave
      INSTANCE_SYNC_TOKEN: "your-secure-32-character-minimum-token"
      # ... other env vars

Notes:

  • All slaves must use the same sync token if they're managed by the same master (or different tokens if configured individually)
  • The master can be configured with slaves via INSTANCE_SLAVES env var or via the UI
  • Synced data includes: proxy hosts, certificates, access lists, and settings
  • CA certificates are synced without their private keys, so slaves validate client certificates but cannot issue them (since v1.13.1)
  • User accounts are NOT synced between instances
  • Each instance can have its own SESSION_SECRET: the master seals synced secrets to each slave's sync key (since v1.13.1). See Feature Guide Instance Sync#sealed-secrets
  • Push-based sync happens automatically on every config change; periodic sync is a safety net
  • Security: Use HTTPS URLs in production. HTTP is only safe within trusted networks (internal Docker networks, private VPCs)

ClickHouse Analytics

Analytics data (traffic events, WAF events) is stored in ClickHouse — a columnar database optimised for fast aggregation queries. Data is deleted by a ClickHouse TTL after CLICKHOUSE_RETENTION_DAYS days (default 30); no manual cleanup is needed.

CLICKHOUSE_PASSWORD

Password for the ClickHouse analytics database. Required.

Generate one:

openssl rand -base64 32

Example:

CLICKHOUSE_PASSWORD="<output of openssl rand -base64 32>"

.env.example leaves it empty, and docker compose refuses to start the clickhouse profile while it is empty.

Important: This password is shared between the web and clickhouse containers via Docker Compose. Changing it requires recreating both containers.


CLICKHOUSE_URL

HTTP endpoint for the ClickHouse server.

Default http://clickhouse:8123
Required No

Only change this if you're running an external ClickHouse instance instead of the bundled container.


CLICKHOUSE_USER

Username for ClickHouse authentication.

Default cpm
Required No

CLICKHOUSE_DB

ClickHouse database name for analytics tables.

Default analytics
Required No

CLICKHOUSE_RETENTION_DAYS

Number of days analytics events (traffic and WAF) are kept before ClickHouse's TTL deletes them. Lower it to use less disk space.

Default 30
Required No

Passed to the web container by docker-compose.yml. Changing it migrates the existing tables' TTL on the next start. A value that is not a positive integer fails with CLICKHOUSE_RETENTION_DAYS must be a positive integer (got: …).


Rootless Docker Operation

Run containers as non-root users for improved security. These are Docker build arguments that must be set before building the images.

PUID (Web Service)

Description: User ID for web container process.

Property Value
Type Number
Required No
Default 10001
Environment Docker build-time (ARG)

Usage:

  • Set non-root user ID for security
  • Match host user for volume permissions

Example:

# In .env or docker-compose.yml
PUID=1000  # Your host user ID

# Find your UID
id -u

Validation: Must be positive integer

Related Variables:

  • PGID (web service)
  • PUID (caddy service - different default)

Notes:

  • Build-time argument (requires rebuild to change: docker compose up --build)
  • Default 10001 avoids system user conflicts
  • Set to host UID for development to match volume permissions
  • Separate from Caddy service which uses PUID=10000

Source: docker-compose.yml:12, docker/web/Dockerfile, .env.example:38-45


PGID (Web Service)

Description: Group ID for web container process.

Property Value
Type Number
Required No
Default 10001
Environment Docker build-time (ARG)

Usage:

  • Set non-root group ID for security
  • Match host group for volume permissions

Example:

# In .env or docker-compose.yml
PGID=1000  # Your host group ID

# Find your GID
id -g

Validation: Must be positive integer

Related Variables:

  • PUID (web service)
  • PGID (caddy service)

Notes:

  • Build-time argument (requires rebuild to change)
  • Default 10001 avoids system group conflicts
  • Usually set to same value as PUID

Source: docker-compose.yml:13, docker/web/Dockerfile


PUID (Caddy Service)

Description: User ID for Caddy container process.

Property Value
Type Number
Required No
Default 10000
Environment Docker build-time (ARG)

Usage:

  • Set non-root user ID for Caddy
  • Different from web service for isolation

Example:

# Different PUID for Caddy vs web
PUID=10000  # Caddy (default)
# Web service uses PUID=10001

Validation: Must be positive integer

Related Variables:

  • PGID (caddy service)
  • PUID (web service - different default)

Notes:

  • Build-time argument (requires rebuild to change)
  • Separate from web service PUID (10000 vs 10001) for security isolation
  • XDG variables use /config and /data directories
  • Caddy config in /config, data in /data

Source: docker-compose.yml:79, docker/caddy/Dockerfile


PGID (Caddy Service)

Description: Group ID for Caddy container process.

Property Value
Type Number
Required No
Default 10000
Environment Docker build-time (ARG)

Usage:

  • Set non-root group ID for Caddy

Example:

PGID=10000  # Caddy (default)

Validation: Must be positive integer

Related Variables:

  • PUID (caddy service)
  • PGID (web service)

Notes:

  • Build-time argument (requires rebuild to change)
  • Separate from web service PGID
  • Usually set to same value as PUID for caddy

Source: docker-compose.yml:80, docker/caddy/Dockerfile


PRIMARY_DOMAIN

Description: Default domain for Caddy configuration.

Property Value
Type String (domain)
Required No
Default caddyproxymanager.com
Environment Runtime (Caddy service)

Usage:

  • Default domain in Caddy configuration
  • Fallback domain for Caddyfile

Example:

PRIMARY_DOMAIN="proxy.example.com"

Validation: None

Related Variables: None

Notes:

  • Used in initial Caddy configuration
  • Can be overridden by proxy host configurations
  • Set in Caddy service environment

Source: docker-compose.yml:91, docker/caddy/Caddyfile


Internal/Build Variables

These variables are set automatically or only relevant during builds. You typically don't need to set them manually.

NODE_ENV

Description: Node.js environment mode.

Property Value
Type String
Required No
Default production (Docker), development (local)
Values development, production, test
Environment All

Usage:

  • Security validation (strict in production)
  • Development features (default credentials allowed)
  • Build optimization

Example:

NODE_ENV=production
NODE_ENV=development

Notes:

  • Automatically set in Dockerfile to production
  • Affects admin credential validation
  • Development mode allows admin/admin credentials
  • Production mode enforces strict password requirements
  • Any runtime value other than development (e.g. staging) gets the production checks too (since v1.13.1)

Source: src/lib/config.ts:12-17, docker-compose.yml:19, docker/web/Dockerfile


NEXT_TELEMETRY_DISABLED

Description: Disable Next.js telemetry.

Property Value
Type Boolean
Required No
Default 1 (disabled in Docker)
Environment Build-time

Example:

NEXT_TELEMETRY_DISABLED=1

Notes:

  • Privacy consideration
  • Set automatically in Dockerfile
  • Disables Next.js anonymous usage data collection

Source: docker/web/Dockerfile


PORT

Description: HTTP port for web service.

Property Value
Type Number
Required No
Default 3000
Environment Runtime

Example:

PORT=3000

Notes:

  • Set automatically in Dockerfile
  • Internal container port
  • External port mapping in docker-compose.yml (ports: "3000:3000")
  • Change requires Dockerfile modification

Source: docker/web/Dockerfile, docker-compose.yml:16


Database-Persisted Settings

These settings are configured through the web UI Settings page and persisted in the database, not environment variables. They are listed here for completeness.

General Settings

Configured via: Settings → General

  • primaryDomain: Default domain for Caddy (string)
  • acmeEmail: Email for Let's Encrypt ACME registration (string)

DNS Provider Settings

Configured via: Settings → DNS Providers

  • defaultProvider: The DNS provider used for ACME DNS-01 challenges by default (string)
  • providers: Array of configured DNS providers, each with encrypted credentials

Supported providers: Cloudflare, Route 53, DigitalOcean, Duck DNS, Hetzner, Vultr, Porkbun, GoDaddy, Namecheap, OVH, IONOS, and Linode. See DNS Provider Configuration for setup guides.

Authentik Settings

Configured via: Settings → Authentik (for Authentik Outpost integration)

  • outpostDomain: Authentik outpost domain (string)
  • outpostUpstream: Authentik outpost upstream URL (string)
  • authEndpoint: (Optional) Auth endpoint override (string)

Metrics Settings

Configured via: Settings → Metrics

  • enabled: Enable Prometheus metrics (boolean)
  • port: Metrics port (number, default 9090)

Logging Settings

Configured via: Settings → Logging

  • enabled: Enable access logging (boolean)
  • format: Log format - "json" or "console" (string)

Note: These settings are NOT environment variables. Configure them through the web UI Settings page after installation. They are stored in the settings table in the SQLite database.


Environment Variable Checklist

Minimal Production Setup

# Required security variables
SESSION_SECRET="<32+ character random string>"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="<12+ chars, mixed case, numbers, special>"
CLICKHOUSE_PASSWORD="<random string, openssl rand -base64 32>"

# Recommended
BASE_URL="https://your-domain.com"

Production with OAuth

# Required security
SESSION_SECRET="..."
ADMIN_USERNAME="..."
ADMIN_PASSWORD="..."
BASE_URL="https://your-domain.com"

# OAuth configuration
OAUTH_ENABLED=true
OAUTH_PROVIDER_NAME="Authentik"
OAUTH_CLIENT_ID="your-client-id"
OAUTH_CLIENT_SECRET="your-client-secret"
OAUTH_ISSUER="https://auth.example.com/application/o/app/"

Custom Rate Limiting

# Required security
SESSION_SECRET="..."
ADMIN_USERNAME="..."
ADMIN_PASSWORD="..."

# Custom rate limiting (add these to the web service's environment: in docker-compose.yml)
LOGIN_MAX_ATTEMPTS=10
LOGIN_WINDOW_MS=600000    # 10 minutes
LOGIN_BLOCK_MS=1800000    # 30 minutes

Forward Auth Behind a CDN or on a Non-Standard Port

# Protected sites reached as https://app.example.com:8443 (Caddy published as "8443:443")
FORWARD_AUTH_ALLOWED_PORTS=8443

# Cloudflare in front of Caddy, origin reachable from Cloudflare only
TRUSTED_CLIENT_IP_HEADER=cf-connecting-ip

Rotating SESSION_SECRET

SESSION_SECRET="<new value from openssl rand -base64 32>"
SESSION_SECRET_PREVIOUS="<old value>"   # remove after one successful start

Then run docker compose up -d. See SESSION_SECRET_PREVIOUS.

Rootless Development

# Required security
SESSION_SECRET="..."
ADMIN_USERNAME="..."
ADMIN_PASSWORD="..."

# Rootless matching host user
PUID=1000  # Your host UID (find with: id -u)
PGID=1000  # Your host GID (find with: id -g)

Note: After setting PUID/PGID, rebuild containers: docker compose up --build -d


Troubleshooting

App Refuses to Start in Production

Symptom: SESSION_SECRET environment variable is required in production, SESSION_SECRET is using a known insecure placeholder value, or docker compose stops with ERROR - SESSION_SECRET is required

Solution:

openssl rand -base64 32

Put the output in SESSION_SECRET in your .env file and run docker compose up -d. If you replace a placeholder secret, stored secrets are re-encrypted automatically; if you replace any other secret, put the old one in SESSION_SECRET_PREVIOUS.


Password Validation Errors

Symptom: Admin credentials validation failed: followed by e.g. ADMIN_PASSWORD must be at least 12 characters long, ADMIN_PASSWORD must include both uppercase and lowercase letters or ADMIN_PASSWORD is an example value from the documentation; choose your own password

Solution:

Ensure password meets ALL requirements:

  • 12+ characters
  • Uppercase (A-Z)
  • Lowercase (a-z)
  • Numbers (0-9)
  • Special characters (!@#$%^&*)
  • Not an example password copied from the documentation or .env.example

Generate one with a password manager; don't reuse an example.


ADMIN_PASSWORD Doesn't Work After Changing It

Symptom: You changed ADMIN_PASSWORD but still can't sign in with it, or the web log shows ADMIN_PASSWORD differs from the stored admin password; keeping the stored password…

Solution:

  • Recreate the container so it sees the new value: docker compose up -d (docker compose restart keeps the old values)
  • On the first start after upgrading from v1.12.0 or earlier, a password changed in the UI is kept. Change ADMIN_PASSWORD once more and run docker compose up -d to force it

See How ADMIN_USERNAME and ADMIN_PASSWORD Are Applied.


OAuth Redirect Mismatch

Symptom: redirect_uri_mismatch error during OAuth login

Solution:

  1. Ensure BASE_URL matches public URL exactly:

    BASE_URL="https://proxy.example.com"  # No trailing slash
  2. Configure OAuth provider redirect URI (check Settings → OAuth Providers for the exact URL):

    {BASE_URL}/api/auth/callback/{provider-id}
    

    Example: https://proxy.example.com/api/auth/callback/authentik-QXV0aG

  3. Recreate the web container after changing BASE_URL: docker compose up -d


Permission Denied Errors (Docker Volumes)

Symptom: Cannot write to /app/data or volume mounts

Solution:

Set PUID/PGID to match host user:

# Find your IDs
id -u  # Your UID
id -g  # Your GID

# Set in .env
PUID=1000
PGID=1000

# Rebuild containers
docker compose down
docker compose up --build -d

Caddy Connection Failed

Symptom: Failed to connect to Caddy API

Solution:

  1. Check Caddy container is running:

    docker ps | grep caddy
  2. Verify CADDY_API_URL is correct:

    # Default for Docker Compose
    CADDY_API_URL="http://caddy:2019"
  3. Test connectivity from web container:

    docker exec caddy-proxy-manager-web wget -O- http://caddy:2019/config/
  4. Ensure both containers are on same network (caddy-network)


OAuth Discovery Fails

Symptom: Cannot auto-discover OAuth endpoints

Solution:

  1. Verify OAUTH_ISSUER is correct and includes trailing slash if required:

    OAUTH_ISSUER="https://auth.example.com/application/o/app/"
  2. Test discovery endpoint manually:

    curl https://auth.example.com/application/o/app/.well-known/openid-configuration
  3. If discovery fails, manually set endpoints:

    OAUTH_AUTHORIZATION_URL="https://..."
    OAUTH_TOKEN_URL="https://..."
    OAUTH_USERINFO_URL="https://..."

Related Documentation

Clone this wiki locally