Repository navigation
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.ymlpasses a fixed list of variables to thewebcontainer. A variable it does not list (for exampleLOGIN_*,AUTH_RATE_LIMIT_*,AUTH_TRUST_HOST,AUTH_ALLOW_OAUTH_ROLE_FROM_CLAIMS,LEGACY_KEY_CUTOFF_DATE, or anyINSTANCE_*variable other thanINSTANCE_SYNC_TIMEOUT_MS) has no effect from.envuntil you add it under thewebservice'senvironment:. 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 (withLOGIN_MAX_ATTEMPTSthat refuses every portal login). After changing.env, recreate the container withdocker compose up -d;docker compose restartkeeps the old values.
- Required Security Variables
- Application Configuration
- OAuth2/OIDC Authentication
- Forward Auth
- Rate Limiting
- Instance Sync
- ClickHouse Analytics
- Rootless Docker Operation
- Internal/Build Variables
- Database-Persisted Settings
- Environment Variable Checklist
- Troubleshooting
The app won't start in production without these three variables set correctly.
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 32Example:
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).
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:
- Set
SESSION_SECRETto the new value andSESSION_SECRET_PREVIOUSto the old one. - Recreate the web container:
docker compose up -d. On startup every stored secret that only an old key decrypts is re-encrypted with the newSESSION_SECRET, logged asRe-encrypted N stored secret(s) with the current SESSION_SECRET. - Remove
SESSION_SECRET_PREVIOUSafter 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_SECRETor inSESSION_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
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
neverto 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_SECRETitself changed, both the current and legacy keys fail — setSESSION_SECRET_PREVIOUSto 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
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.
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.examplefiles 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).
(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_PASSWORDno longer signs in until you change it. -
Recovering a lost admin password: set
ADMIN_PASSWORD(orADMIN_USERNAME) to a new value and recreate the web container withdocker compose up -d(docker compose restartkeeps the old values). This resets the primary admin's password and its username toADMIN_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_USERNAMEused by another account (since v1.13.1): a newADMIN_USERNAMEthat 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, afterFailed 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_PASSWORDafter the primary admin's username or email was changed on the Users page: applying the environment credentials resets them toADMIN_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,adminor a documented example,ADMIN_PASSWORDis 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_USERNAMEis 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
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
Description: Caddy Admin API endpoint for configuration management.
| Property | Value |
|---|---|
| Type | String (URL) |
| Required | No |
| Default | Production: http://caddy:2019Development: 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
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 pathValidation: 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/-shmfiles 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
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
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 enables single sign-on with identity providers like Authentik, Keycloak, Auth0, etc.
Description: Enable OAuth2/OIDC authentication.
| Property | Value |
|---|---|
| Type | Boolean |
| Required | No |
| Default | false |
| Environment | All |
Example:
OAUTH_ENABLED=true
OAUTH_ENABLED=falseValidation: 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
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
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
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
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
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
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
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
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=falseValidation: 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
falsefor more secure manual linking
Source: src/lib/config.ts, docker-compose.yml
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=trueValidation: 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=falseto permit onboarding only through OAuth/OIDC. - OAuth-created users receive the safe default
userrole unless trusted role claims are explicitly enabled separately.
Source: src/lib/config.ts, src/lib/auth-server.ts, docker-compose.yml
Variables for the built-in forward auth portal. See Feature Guide Forward Auth.
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,9443Validation: 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
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.
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 recommendedDescription: 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 minutesRelated Variables: 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 strictRelated Variables: AUTH_RATE_LIMIT_WINDOW
Notes:
- In-memory tracking (not suitable for multi-instance deployments)
- Applies to Better Auth's
/api/authendpoints 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
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 HostNotes:
- When
false(default), Better Auth usesBASE_URLto construct callback URLs — this is the secure default - Only set to
trueif your reverse proxy rewrites theHostheader and does not setX-Forwarded-Host - Setting to
truein an exposed environment could allow Host header poisoning
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_ATTEMPTSfailures from one client, or from one client against one account, withinLOGIN_WINDOW_MSblock that client (for that account) forLOGIN_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_MSif 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 forLOGIN_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_HEADERor the rightmostX-Forwarded-Forentry; a client-sentX-Real-IPis no longer trusted.
See Feature Guide Forward Auth#portal-login-rate-limits for details.
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-ipValidation: 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-IPandCF-Connecting-IPthrough unchanged, and the rightmostX-Forwarded-Forentry is then the real client - Behind a CDN the rightmost
X-Forwarded-Forentry 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-Forand the per-IP limits are only best effort; expose the portal (BASE_URL) through Caddy or another proxy that overwritesX-Forwarded-For - Passed to the web container by
docker-compose.yml
Source: src/lib/client-ip.ts, docker-compose.yml
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).
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=standaloneValidation: 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
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/syncendpoint - Tokens stored in the database are encrypted at rest using
SESSION_SECRET
Source: src/lib/instance-sync.ts
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
urlmust pass the same checks as instances added in the UI:http(s)only, no credentials, query string or fragment. Otherwise the master logsSkipping INSTANCE_SLAVES entry <index>: <reason>, e.g.Base URL must not contain a query string or fragment(since v1.13.1) -
syncKeyIdmust be 16 lowercase hex characters andsyncPublicKeybase64 of 32 bytes; when both are set they must match. Otherwise the entry is skipped withSkipping 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) orsyncKeyId(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'sSESSION_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
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=0Validation: 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
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=falseValidation: 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
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=120000Validation: 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(default60000)
Source: src/lib/instance-sync.ts, docker-compose.yml
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=5242880Validation: Must be a positive integer
Related Variables: INSTANCE_MODE
Notes:
- Applies to
/api/instances/syncon slave instances - Payloads larger than the limit return HTTP 413
Source: app/api/instances/sync/route.ts
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=10Validation: Must be a positive integer
Related Variables: INSTANCE_SYNC_RATE_WINDOW_MS
Notes:
- Applies to
/api/instances/syncon 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
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=300000Validation: Must be a positive integer
Related Variables: INSTANCE_SYNC_RATE_MAX
Notes:
- Applies to
/api/instances/syncon slave instances
Source: app/api/instances/sync/route.ts
# 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 receives configuration from master
INSTANCE_MODE=slave
INSTANCE_SYNC_TOKEN="your-secure-32-character-minimum-token"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 varsNotes:
- 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_SLAVESenv 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)
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.
Password for the ClickHouse analytics database. Required.
Generate one:
openssl rand -base64 32Example:
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.
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.
Username for ClickHouse authentication.
| Default | cpm |
| Required | No |
ClickHouse database name for analytics tables.
| Default | analytics |
| Required | No |
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: …).
Run containers as non-root users for improved security. These are Docker build arguments that must be set before building the images.
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 -uValidation: 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
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 -gValidation: 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
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=10001Validation: 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
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
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
These variables are set automatically or only relevant during builds. You typically don't need to set them manually.
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=developmentNotes:
- 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
Description: Disable Next.js telemetry.
| Property | Value |
|---|---|
| Type | Boolean |
| Required | No |
| Default |
1 (disabled in Docker) |
| Environment | Build-time |
Example:
NEXT_TELEMETRY_DISABLED=1Notes:
- Privacy consideration
- Set automatically in Dockerfile
- Disables Next.js anonymous usage data collection
Source: docker/web/Dockerfile
Description: HTTP port for web service.
| Property | Value |
|---|---|
| Type | Number |
| Required | No |
| Default | 3000 |
| Environment | Runtime |
Example:
PORT=3000Notes:
- 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
These settings are configured through the web UI Settings page and persisted in the database, not environment variables. They are listed here for completeness.
Configured via: Settings → General
- primaryDomain: Default domain for Caddy (string)
- acmeEmail: Email for Let's Encrypt ACME registration (string)
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.
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)
Configured via: Settings → Metrics
- enabled: Enable Prometheus metrics (boolean)
- port: Metrics port (number, default 9090)
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.
# 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"# 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/"# 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# 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-ipSESSION_SECRET="<new value from openssl rand -base64 32>"
SESSION_SECRET_PREVIOUS="<old value>" # remove after one successful startThen run docker compose up -d. See SESSION_SECRET_PREVIOUS.
# 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
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 32Put 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.
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.
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 restartkeeps 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_PASSWORDonce more and rundocker compose up -dto force it
See How ADMIN_USERNAME and ADMIN_PASSWORD Are Applied.
Symptom: redirect_uri_mismatch error during OAuth login
Solution:
-
Ensure
BASE_URLmatches public URL exactly:BASE_URL="https://proxy.example.com" # No trailing slash
-
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 -
Recreate the web container after changing BASE_URL:
docker compose up -d
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 -dSymptom: Failed to connect to Caddy API
Solution:
-
Check Caddy container is running:
docker ps | grep caddy -
Verify
CADDY_API_URLis correct:# Default for Docker Compose CADDY_API_URL="http://caddy:2019"
-
Test connectivity from web container:
docker exec caddy-proxy-manager-web wget -O- http://caddy:2019/config/ -
Ensure both containers are on same network (
caddy-network)
Symptom: Cannot auto-discover OAuth endpoints
Solution:
-
Verify OAUTH_ISSUER is correct and includes trailing slash if required:
OAUTH_ISSUER="https://auth.example.com/application/o/app/" -
Test discovery endpoint manually:
curl https://auth.example.com/application/o/app/.well-known/openid-configuration
-
If discovery fails, manually set endpoints:
OAUTH_AUTHORIZATION_URL="https://..." OAUTH_TOKEN_URL="https://..." OAUTH_USERINFO_URL="https://..."
- Installation Guide - Complete setup instructions
- Security Configuration - Production security best practices
- OAuth Authentication Setup - Provider-specific OAuth guides
- DNS Provider Configuration - DNS-01 challenge provider setup
- Feature Guide Forward Auth - Forward auth portal, ports and login limits
- Feature Guide Instance Sync - Master/slave sync and sync key pinning
- Rootless Docker Operation - Detailed PUID/PGID guide
- Troubleshooting - Common issues and solutions