Repository navigation
Troubleshooting
When things don't work.
- Installation Issues
- Authentication Issues
- Certificate Issues
- OAuth Issues
- Permission Issues
- Networking Issues
- Database Issues
- Performance Issues
- Secret Decryption Issues
- Forward Auth Issues
- Instance Sync Issues
- WAF Issues
Symptom: Container exits immediately after starting
Solutions:
-
Check logs:
docker compose logs web docker compose logs caddy
-
Common causes:
- Missing required environment variables
- A
SESSION_SECRETorADMIN_PASSWORDcopied from the documentation (see App Refuses to Start: Example Secret or Password) - Port conflicts
- Permission denied
-
Verify environment variables:
docker compose config
-
Check for port conflicts:
lsof -i :3000 lsof -i :80 lsof -i :443
Error: docker compose up stops before creating any container with a message ending in ERROR - SESSION_SECRET is required, ERROR - ADMIN_PASSWORD is required or ERROR - CLICKHOUSE_PASSWORD is required
Cause: .env.example leaves SESSION_SECRET, ADMIN_PASSWORD and CLICKHOUSE_PASSWORD empty (since v1.13.1), and docker-compose.yml refuses to start while a required one is empty. CLICKHOUSE_PASSWORD is only required while the clickhouse profile is active.
Solution: fill them in .env:
openssl rand -base64 32 # use one output for SESSION_SECRET, another for CLICKHOUSE_PASSWORDChoose your own ADMIN_PASSWORD (12+ characters, upper- and lowercase, a digit and a symbol), then run docker compose up -d.
Symptom: Migration failed or database errors on startup
Solutions:
Option 1: Reset database (deletes all data):
docker compose down
rm -rf data/caddy-proxy-manager.db*
docker compose up -dOption 2: Check permissions:
ls -la data/
chmod 600 data/caddy-proxy-manager.dbOption 3: Restore from backup:
cp backup/caddy-proxy-manager.db data/
docker compose restart webSymptom: Error: bind: address already in use
Solutions:
-
Find process using port:
# Port 3000 lsof -i :3000 # Port 80 lsof -i :80 # Port 443 lsof -i :443
-
Stop conflicting service:
sudo systemctl stop apache2 # or nginx, etc. -
Or change ports in docker-compose.yml:
ports: - "8080:3000" # Web UI on 8080 - "8000:80" # HTTP on 8000 - "8443:443" # HTTPS on 8443
If you move Caddy off 80/443 and use forward auth, set
FORWARD_AUTH_ALLOWED_PORTS=8000,8443(see Portal Says the Port Is Not Allowed); if you move the web UI, updateBASE_URL.
Error: SESSION_SECRET environment variable is required in production
Solution:
-
Generate session secret:
openssl rand -base64 32
-
Add to .env file:
SESSION_SECRET="<output of openssl rand -base64 32>" -
Recreate the web container (
docker compose restartdoes not re-read.env):docker compose up -d
Error: SESSION_SECRET is using a known insecure placeholder value. … or Admin credentials validation failed: with ADMIN_PASSWORD is an example value from the documentation; choose your own password
Cause: Since v1.13.1, the web container refuses the placeholder secrets (including your-secure-session-secret-here-min-32-chars from older .env.example files) and the example admin passwords from earlier READMEs and .env.example files.
Solution:
- Generate a new secret with
openssl rand -base64 32and choose your ownADMIN_PASSWORD. - Run
docker compose up -d.
Stored secrets encrypted under a placeholder secret are re-encrypted with the new one automatically on that start; nothing has to be re-entered. Replacing any other secret needs SESSION_SECRET_PREVIOUS (see Security Configuration#secret-rotation).
Error: Admin credentials validation failed: followed by e.g. ADMIN_PASSWORD must be at least 12 characters long or ADMIN_PASSWORD must include both uppercase and lowercase letters. In the UI and API: "Password must be at least 12 characters long, …" (or "New password …" when changing it).
Solution:
Ensure password meets ALL requirements:
- 12+ characters (at most 256 for passwords set in the app)
- Uppercase letters (A-Z)
- Lowercase letters (a-z)
- Numbers (0-9)
- Special characters (!@#$%^&*)
- Not an example password from the documentation
Example:
ADMIN_PASSWORD="<choose-your-own: 12+ chars, upper, lower, digit, symbol>"Since v1.13.1 the same policy also applies to users created by an admin and to self-registration; password changes already enforced it.
Symptom: Can't log in even with correct username/password
Solutions:
-
Check environment variables loaded:
docker compose exec web env | grep ADMIN
-
Recreate the web container after .env changes (
docker compose restartkeeps the old values):docker compose up -d
-
Password changed in the UI? Since v1.13.1,
ADMIN_PASSWORDis applied only when the admin is created or whenADMIN_USERNAME/ADMIN_PASSWORDchange, so a password changed on the Profile page is kept across restarts and the old environment value no longer works.- To reset it, set
ADMIN_PASSWORDto a new value and rundocker compose up -d. This also restores the admin role, re-activates the account if it was disabled and signs out its sessions. - On the first start after upgrading, a stored password that differs from
ADMIN_PASSWORDis kept, with the warningADMIN_PASSWORD differs from the stored admin password; keeping the stored password because it was probably changed in the UI. …. ChangeADMIN_PASSWORDonce more to force it. - Since v1.13.1, a changed
ADMIN_USERNAMEthat another account already signs in with, or has as its email address, is not applied and nothing changes (log:ADMIN_USERNAME "…" is not applied: another account already signs in with it or has it as its email address. …). Sign in with the previous username, or give that account a different username or email address on the Users page first. - 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. Choose anADMIN_USERNAMEno other account uses, or have another administrator give that account a different username or email address first.
See Environment Variables Reference#how-admin_username-and-admin_password-are-applied.
- To reset it, set
-
Use the sign-in username: the login page signs in by username (ignoring case), not by email. Since v1.13.1 the username is the account's email address only when that address is a valid username (3–255 characters from
A-Z a-z 0-9 _ . @ -, so not one containing+) that no other account uses; otherwise an administrator sets it. The user's Profile page shows it as Sign-in username. If it shows none, see No Sign-in Username on the Profile Page. -
Check for rate limiting (see Rate Limited):
- Dashboard login: wait 10 seconds
- Forward auth portal: wait
LOGIN_BLOCK_MS(15 minutes by default) - Or restart:
docker compose restart web
Error: "Please sign in again before setting a password."
Cause: Since v1.13.1, an account without a password can add one only within 10 minutes of signing in.
Solution: Sign out, sign in again with OAuth, and set the password right away on the Profile page.
Symptom: The Profile page shows no Sign-in username and says "Your account has no sign-in username the login page can use, so you cannot sign in there with a password. An administrator has to set a sign-in username for your account. This page then shows it." (on an account without a password: "…, then you can set your password here."). The Users page shows no sign-in username for the account.
Cause: Since v1.13.1, CPM no longer makes a username up from an email address. An account gets its own email address as username only when that address is 3–255 characters from A-Z a-z 0-9 _ . @ - (an address with + is not) and no other account signs in with it or has it as its email address. Older releases could also store a username the login page cannot use, such as an email with + or a mixed-case username.
Solution: An administrator sets a username:
- Open Users, click the edit icon on the account, enter a Username and click Save, or send
PUT /api/v1/users/:idwith{"username": "alice"}(see Feature Guide REST API#users). - Use 3–255 characters of lowercase letters, digits and
_ . @ -that are not another account's username, email address or forward-auth portal name; otherwise the save is refused with the reason (e.g. "Another account already signs in with this name or has it as its email address"). - Tell the user. They sign in at
/loginwith that username and their password; an account without a password sets one on the Profile page first.
If the Profile page instead says "Your password cannot be used on the sign-in page yet. Change it once here to enable password sign-in.", either the account's own email address qualifies as a username or its password was set on an older release. Changing the password once (signed in another way, such as with OAuth) enables password sign-in, and in the first case makes the email address the username. In the first case an administrator can also set a username as above.
See Feature Guide User Management#sign-in-usernames.
Symptom: Since v1.13.1, the web container log shows warnings like these on every start:
Sign-in username "alice-cpm@example.com" of user 5 is an email address other than the account's own and can be somebody else's; check it on the Users page
Sign-in username "bob" of user 7 is also another account's username, email address or forward-auth portal name; give one of them a different username on the Users page
Cause: CPM checks the stored sign-in usernames on every start and reports, without changing anything:
-
An email address other than the account's own. Usually a username the withdrawn v1.13.0 release generated from the email address (
alice+cpm@example.com→alice-cpm@example.com, oralice-2@example.comafter a collision), or the account's address before its email changed. Somebody else can own that address. -
A name that also reaches another account: another account's username, email address or forward-auth portal name (the
<name>of an email<name>@localhost), ignoring case.
v1.13.0 logged the usernames it generated on startup as Gave user <id> the sign-in username <username>, but not those it generated on account creation or password and profile changes; this check covers both.
Solution:
- Find the account:
GET /api/v1/users/<id>returns its email address, which the Users page search finds. - Click the edit icon and set a Username the user should sign in with, such as one that is not an email address (
alice) or the account's own email address when it qualifies. For a shared name, give one of the accounts a different username, or change the email address that collides. - Tell the user their new username.
The warning stops once the username no longer matches. See Feature Guide User Management#upgrading-from-v1130.
Symptom: "Too many requests. Please try again later." on the dashboard login, "Too many login attempts. Please try again later." on the forward auth portal, or "Too many attempts. Please try again later." when changing a password
Solutions:
Option 1: Wait. Dashboard sign-in allows 3 requests per 10 seconds per client (Better Auth's built-in limit, which only AUTH_RATE_LIMIT_ENABLED=false turns off). Portal logins and password changes are blocked for LOGIN_BLOCK_MS (15 minutes by default).
Option 2: Restart container (clears in-memory rate limit):
docker compose restart webOption 3: Adjust rate limiting:
The forward auth portal login, password changes and account linking use the LOGIN_* variables. AUTH_RATE_LIMIT_MAX/AUTH_RATE_LIMIT_WINDOW only affect Better Auth's /api/auth endpoints other than sign-in and sign-up. The stock docker-compose.yml passes none of them through, so add them to the web service's environment: and run docker compose up -d:
services:
web:
environment:
LOGIN_MAX_ATTEMPTS: "10" # More attempts allowed
LOGIN_BLOCK_MS: "300000" # Shorter block (5 min)The portal also limits failures per account from all clients combined (65 per hour with the defaults), so an account can be blocked even when your own client is not. See Environment Variables Reference#rate-limiting.
Symptom: HTTPS not working, shows Caddy default page
Solutions:
-
Check Caddy logs:
docker compose logs caddy | grep -i acme docker compose logs caddy | grep -i certificate
-
Verify DNS resolves:
dig +short your-domain.com # Should return your server's public IP -
Check ports accessible:
# From external machine nc -zv your-server-ip 80 nc -zv your-server-ip 443 -
Verify ACME email set:
- Settings → General → ACME Email
-
Check Let's Encrypt rate limits:
- Visit crt.sh
- Search for your domain
- If 50+ certs/week, you're rate limited
Error: Failed to obtain certificate
Common causes:
-
DNS not propagated:
- Wait up to 48 hours for DNS propagation
- Verify:
nslookup your-domain.com
-
Firewall blocking:
- Check server firewall:
sudo ufw status - Check cloud provider security groups
- Check server firewall:
-
Domain doesn't point to server:
- Verify IP:
dig +short your-domain.com - Should match:
curl ifconfig.me
- Verify IP:
-
Cloudflare proxy enabled incorrectly:
- Disable Cloudflare proxy (orange cloud → gray cloud)
- Or configure DNS-01 challenge
Symptom: Wildcard domain *.example.com doesn't get certificate
Solution:
Wildcard certificates require DNS-01 challenge:
-
Configure a DNS provider:
-
Verify configuration:
- Settings → DNS Providers → provider configured and set as default
-
Check logs:
docker compose logs caddy | grep -i dns
Error: redirect_uri_mismatch
Solutions:
-
Verify BASE_URL:
# In .env BASE_URL="https://proxy.example.com" # No trailing slash
-
Configure redirect URI in OAuth provider:
{BASE_URL}/api/auth/callback/{provider-id}The exact callback URL is shown in Settings → OAuth Providers.
-
Recreate the web container after changes (
docker compose restartdoes not re-read.env):docker compose up -d
Error: Failed to discover OIDC endpoints
Solutions:
-
Test discovery endpoint:
curl https://auth.example.com/application/o/app/.well-known/openid-configuration
-
Check trailing slash in OAUTH_ISSUER:
# Some providers require trailing slash OAUTH_ISSUER="https://auth.example.com/application/o/app/"
-
Use manual endpoints if discovery fails:
OAUTH_AUTHORIZATION_URL="https://..." OAUTH_TOKEN_URL="https://..." OAUTH_USERINFO_URL="https://..."
Symptom: OAuth login option doesn't appear on login page
Solutions:
-
Verify OAUTH_ENABLED:
OAUTH_ENABLED=true # Must be lowercase "true" -
Check all required variables:
OAUTH_CLIENT_ID="..." OAUTH_CLIENT_SECRET="..." OAUTH_ISSUER="..."
-
Recreate the web container (
docker compose restartdoes not re-read.env):docker compose up -d
-
Check logs:
docker compose logs web | grep -i oauth
Important: As of v1.0, Caddy Proxy Manager uses Docker named volumes by default instead of bind mounts.
If you're experiencing permission issues:
-
Using default Docker volumes (recommended): Permission issues should be rare with volumes as Docker handles permissions automatically.
-
Switched to bind mounts (./data, ./caddy-data, etc.): You need to ensure proper file ownership matches the container's PUID/PGID.
Quick fix for bind mount permission issues:
# Stop containers
docker compose down
# Fix ownership (use default container UIDs)
sudo chown -R 10001:10001 data/
sudo chown -R 10000:10000 caddy-data/ caddy-config/ caddy-logs/
# Or rebuild containers to match your user
PUID=$(id -u) PGID=$(id -g) docker compose up --build -d
sudo chown -R $(id -u):$(id -g) data/ caddy-data/ caddy-config/ caddy-logs/For detailed information including:
- How to switch between volumes and bind mounts
- Step-by-step permission troubleshooting
- Migration guides
- Advanced ACL configurations
See: Rootless Docker Operation#docker-volumes-vs-bind-mounts
Error: EACCES: permission denied, open '/app/data/caddy-proxy-manager.db'
Solutions:
Option 1: Fix ownership:
sudo chown -R 10001:10001 data/
docker compose restart webOption 2: Use rootless with your user:
PUID=$(id -u) PGID=$(id -g) docker compose up --build -dSee Rootless Docker Operation.
Symptom: Permission denied when editing files in data/ directory
Solutions:
Option 1: Change PUID to your user (recommended for dev):
PUID=$(id -u) PGID=$(id -g) docker compose up --build -dOption 2: Use sudo:
sudo nano data/caddy-proxy-manager.dbOption 3: Add yourself to group:
sudo groupadd -g 10001 caddypm
sudo usermod -aG caddypm $(whoami)
# Log out and back in
sudo chgrp -R caddypm data/
sudo chmod -R g+rw data/Symptom: After upgrading, a host-side backup or monitoring job gets Permission denied on caddy-proxy-manager.db (or its -wal/-shm files)
Cause: Since v1.13.1, the web container removes the world permission bits from the database files whenever it opens the database, because they hold password hashes, session tokens and encrypted secrets. Owner and group bits are left unchanged.
Solution: Run the job as the files' owner, or give it the files' group (as in Option 3 above, with read access: sudo chmod g+r data/caddy-proxy-manager.db*). Adding world read access back does not last: it is removed again on the next start. If the container cannot change the mode, it logs Could not restrict permissions on <file>: … and keeps running.
Error: Failed to connect to Caddy API at http://caddy:2019
Solutions:
-
Check Caddy container running:
docker ps | grep caddy -
Verify network connectivity:
docker compose exec web wget -O- http://caddy:2019/config/ -
Check CADDY_API_URL:
# In .env or docker-compose.yml CADDY_API_URL="http://caddy:2019"
-
Verify same Docker network:
docker network inspect caddy-network
Symptom: Caddy returns 502 error for proxy host
Solutions:
-
Check upstream service running:
curl http://upstream-host:port
-
For Docker containers, verify network:
docker network inspect caddy-network
-
Check container name correct:
docker ps --format "{{.Names}}" -
Use host.docker.internal for host services:
# macOS/Windows http://host.docker.internal:8080 # Linux http://172.17.0.1:8080 -
Check logs:
docker compose logs caddy | grep -i upstream
Symptom: WebSocket connections fail through proxy
Solutions:
-
Verify backend supports WebSocket
-
Add WebSocket headers:
Connection: {http.request.header.Connection} Upgrade: {http.request.header.Upgrade} -
Check for timeout issues:
- WebSocket connections are long-lived
- May need timeout adjustments (future feature)
Error: database is locked
Solutions:
-
Close all connections:
docker compose restart web
-
Check for multiple instances:
docker ps | grep caddy-proxy-manager # Should see only 1 web container
-
Remove lock file:
rm -f data/caddy-proxy-manager.db-wal rm -f data/caddy-proxy-manager.db-shm docker compose restart web
Symptom: Database errors, data missing, crashes
Solutions:
Option 1: Restore from backup:
docker compose down
cp backup/caddy-proxy-manager.db data/
docker compose up -dOption 2: Export and recreate:
# Export (if possible)
sqlite3 data/caddy-proxy-manager.db .dump > backup.sql
# Recreate
rm data/caddy-proxy-manager.db
docker compose up -d
# Import (manual process)Option 3: Start fresh:
docker compose down
rm -rf data/
docker compose up -dSolutions:
-
Check system resources:
docker stats
-
Increase container resources:
# docker-compose.yml services: web: deploy: resources: limits: memory: 1G cpus: '1.0'
-
Check upstream performance:
- Test upstream directly
- May be backend slowness
-
Enable metrics for monitoring:
- Settings → Metrics → Enable
Solutions:
-
Check logs size:
docker compose logs --tail=100 web
-
Limit log size:
# docker-compose.yml services: web: logging: options: max-size: "10m" max-file: "3"
-
Restart containers periodically:
docker compose restart
Error:
[secret] Failed to decrypt stored secret for DNS provider "cloudflare" credential "api_token": decryption failed with the current key and SESSION_SECRET_PREVIOUS, and the legacy key grace period has expired. ...
or, at startup:
[secret] setting "dns_provider" cannot be decrypted with SESSION_SECRET or SESSION_SECRET_PREVIOUS; re-enter it in the UI or set SESSION_SECRET_PREVIOUS to the secret it was stored with.
N stored secret(s) listed above could not be decrypted with SESSION_SECRET or SESSION_SECRET_PREVIOUS; ...
Releases up to v1.12.0 print [secret] Failed to decrypt stored secret …: HKDF decryption failed and the legacy key grace period has expired. … instead.
What's happening:
Sensitive values are stored in the database encrypted at rest (enc:v1: prefix) using AES-256-GCM with a key derived from SESSION_SECRET. If SESSION_SECRET changes — for example after your environment variables were reset, or you generated a new secret — previously stored values can no longer be decrypted unless the old secret is in SESSION_SECRET_PREVIOUS.
Since v1.13.1, the web container tries every stored secret at startup: values that SESSION_SECRET_PREVIOUS (or a public placeholder secret) decrypts are re-encrypted with the current SESSION_SECRET, values that no key decrypts are left as stored and listed in the log, and OAuth sign-in tokens that no key decrypts are cleared (Cleared N stored OAuth sign-in token(s) that no key decrypts; …), since CPM does not use them.
Data stored encrypted with SESSION_SECRET:
- DNS provider credentials, e.g. the Cloudflare API token (Settings → DNS Providers) — used for DNS-01 challenges. Credentials saved in plaintext by older versions (e.g. through
PUT /api/v1/settings/dns-provider) are encrypted at startup, logged asEncrypted N DNS provider credential(s) that were stored in plaintext - OAuth provider client ID / secret (Settings → OAuth Providers)
- OAuth sign-in tokens of linked accounts
- Instance API tokens (Instances)
- Instance sync master token (Settings → Instance Sync)
- Imported certificate private keys (Certificates)
- CA private keys (Certificates → CA / mTLS), since v1.13.1
Solutions:
Option 1: Set SESSION_SECRET_PREVIOUS (recommended if you still have the old secret)
Keep the new SESSION_SECRET, put the old value (e.g. from a backup of your .env file or compose config) in SESSION_SECRET_PREVIOUS, and run docker compose up -d. Startup re-encrypts everything with the new secret (Re-encrypted N stored secret(s) with the current SESSION_SECRET); remove SESSION_SECRET_PREVIOUS after that start. See Security Configuration#secret-rotation.
Option 2: Re-enter the affected token/secret
Open the relevant settings page, re-enter the token/password field, and save. The value is re-encrypted with the current key. For example, if the error mentions a DNS provider credential:
- Go to Settings → DNS Providers
- Edit the provider named in the error (e.g.
cloudflare) - Re-enter the API token (reset it with your DNS provider if needed)
- Save
Option 3: Restore the previous SESSION_SECRET
Restoring the old value as SESSION_SECRET also makes all stored values decryptable again. To move to a new secret later, use Option 1.
Option 4: LEGACY_KEY_CUTOFF_DATE (legacy-format secrets only)
Secrets stored by older versions used a legacy key derivation. They are supported only until a cutoff date (default 2026-06-01). If your SESSION_SECRET is unchanged but you see the grace-period error above, you can temporarily extend or disable the cutoff:
# In .env — ISO 8601 date, or "never" to disable entirely
LEGACY_KEY_CUTOFF_DATE=neverNote: This does not help if
SESSION_SECRETitself changed. The legacy key is also derived fromSESSION_SECRET, so a changed secret breaks both keys. Use Option 1, 2 or 3 instead.
Find which stored values are encrypted:
sqlite3 data/caddy-proxy-manager.db "SELECT key FROM settings WHERE value LIKE '%enc:v1:%';"DNS provider credentials, the instance sync master token, and other settings live in the settings table; instance API tokens are in the instances table (apiToken column), certificate private keys in the certificates table and CA private keys in the ca_certificates table (privateKeyPem column in both).
See Environment Variables Reference for SESSION_SECRET, SESSION_SECRET_PREVIOUS and LEGACY_KEY_CUTOFF_DATE details.
Error: "The CA private key cannot be decrypted with the current SESSION_SECRET. Restore the previous secret via SESSION_SECRET_PREVIOUS or create a new CA." in the Issue Client Certificate dialog
Cause: Since v1.13.1, CA private keys are encrypted with SESSION_SECRET, and this CA's key was stored under a secret that is no longer configured.
Solution: Set SESSION_SECRET_PREVIOUS to the secret the key was stored with and run docker compose up -d (the key is then re-encrypted with the current secret), or create a new CA. Client certificates already issued keep working, because validating them only needs the CA certificate.
On an instance sync slave, "This CA has no stored private key — cannot issue client certificates" is expected: CA private keys stay on the master. Issue client certificates on the master.
Error: 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 log shows:
[forward-auth] Rejected app.example.com:8443 because port 8443 is not listed in FORWARD_AUTH_ALLOWED_PORTS. If forward-auth protected sites are served on port 8443, add it to FORWARD_AUTH_ALLOWED_PORTS (comma-separated) and recreate the web container (docker compose up -d).
Cause: Since v1.13.1, logins, redirects and sessions for protected sites on a port other than 80/443 are refused unless the port is listed in FORWARD_AUTH_ALLOWED_PORTS, because Caddy matches proxy hosts by hostname only. After upgrading, existing forward-auth sessions on such a port stop validating too.
Solution:
# In .env (docker-compose.yml passes it to the web container)
FORWARD_AUTH_ALLOWED_PORTS=8443Then run docker compose up -d. See Environment Variables Reference.
Symptom: Portal logins fail with "Too many login attempts. Please try again later." for many users together, typically behind a CDN or another proxy in front of Caddy
Cause: Since v1.13.1, the portal counts failures per client using the rightmost X-Forwarded-For entry and no longer trusts a client-sent X-Real-IP. Behind a CDN, that entry is the CDN edge, so users behind the same edge share one limit.
Solution:
- Behind a CDN, set
TRUSTED_CLIENT_IP_HEADERto the CDN's client IP header (e.g.cf-connecting-ip) and rundocker compose up -d, but only if the origin accepts connections from the CDN alone; otherwise clients can forge the header. - Leave it unset when Caddy is the outermost proxy.
- A single account can also be blocked by failures from many clients combined (65 per hour with the defaults); wait
LOGIN_BLOCK_MS(15 minutes by default) or restart the web container.
See Feature Guide Forward Auth#portal-login-rate-limits.
Sync errors appear on the master under Settings → Instance Sync for each slave; slaves configured with INSTANCE_SLAVES report them in the master's log. See Feature Guide Instance Sync for sealed sync and sync key pinning (since v1.13.1).
Error: "Slave sync key changed; verify the slave, then pin its new key or reset its key pin", with this in the master's log:
Instance sync: slave "<name>" presented sync key <new>, but <pinned> is pinned and the slave sent no valid rotation proof; not syncing…
Cause: The master pinned the slave's sync key, which the slave derives from its SESSION_SECRET, and the slave now presents another one: its SESSION_SECRET changed (reinstall, or a rotation without SESSION_SECRET_PREVIOUS or with it removed too early), or something else answers at the slave's URL.
Solution:
-
Rotated secret: put the old value in the slave's
SESSION_SECRET_PREVIOUS, rundocker compose up -don the slave, then click Sync now on the master. The master logsInstance sync: slave "<name>" proved its new sync key <new> with the pinned key <old>; pinned the new key. Only then removeSESSION_SECRET_PREVIOUSfrom the slave. - Otherwise: compare the key id in the master's log with the one the slave shows on its own Settings → Instance Sync page. If they match, pin the slave's new key on the master (Key pin). A key the slave does not show means something else answered at its address.
For a slave pinned with syncPublicKey or syncKeyId in INSTANCE_SLAVES, the error is "Slave sync key does not match the key configured in INSTANCE_SLAVES": update the entry with the slave's new key.
Error: "Sync timed out"
Cause: Since v1.13.1, the master stops a sync request after INSTANCE_SYNC_TIMEOUT_MS (default 60 seconds), which covers the upload and the slave's apply including its Caddy reload. The slave may still finish applying the configuration.
Solution: Check the slave's logs and reachability. For large configurations or slow slaves, raise INSTANCE_SYNC_TIMEOUT_MS on the master (at most 300000) and run docker compose up -d.
Error: "Sync key request failed with HTTP ", "Slave returned an invalid sync key" or "Slave did not acknowledge the sync (unexpected response)"
Cause: Before every sync the master fetches the slave's sync key with GET /api/instances/sync (since v1.13.1), and it does not follow redirects.
| Error | Usual cause |
|---|---|
HTTP 401 |
Wrong sync token |
HTTP 404 |
Wrong base URL, or a proxy or virtual host answering instead of CPM |
HTTP 302, "Slave returned an invalid sync key", "Slave did not acknowledge the sync"
|
A login page, redirect or other non-CPM response in front of the slave |
HTTP 405 |
The slave has a pinned key but answers like v1.12.0 or earlier: it was downgraded, or a proxy refuses GET
|
Solution: Proxies in front of a slave must pass both GET and POST on /api/instances/sync, with the Authorization header and the query string, and must not cache the GET reply. After deliberately downgrading a slave to v1.12.0 or earlier, reset its key pin on the master.
Symptom: The slave's Settings → Instance Sync page shows "Failed to apply synchronized configuration" while master and slaves run different releases
Cause: A master on v1.12.0 or earlier sends DNS provider credentials encrypted with its own SESSION_SECRET, and a slave on v1.12.0 or earlier expects them that way from any master.
Solution: Upgrade slaves before, or together with, the master. Until the master is upgraded, give each slave the master's secret as SESSION_SECRET or in SESSION_SECRET_PREVIOUS; until a slave is upgraded, it needs the master's current secret as its own SESSION_SECRET.
Symptom: The web log shows
[waf] global WAF settings: 1 custom directive line(s) are not sent to Caddy and have no effect:
"<line>" → <reason>
(or the same for proxy host "<name>" (<domains>)), or saving a host or the WAF settings is rejected with waf.custom_directives contains N line(s) that will be dropped and never sent to Caddy: …
Cause: Directives outside the allowed set (such as Include) were never sent to Caddy. Since v1.13.1, rules that could read files, run programs or switch the WAF off (for example @pmFromFile, @inspectFile, @validateSchema, setenv, ctl:ruleEngine), lines Coraza cannot parse, directives continued over several lines with a trailing \, and rules reusing the id: of an earlier rule are left out as well. When one rule of a chain is dropped, the whole chain is dropped. Rules stored by older versions are kept but only reported in the log (with the source they come from); a save is rejected only for lines it newly drops.
Solution: Rewrite or remove each listed line according to its reason, write each directive on one line, and give every rule a unique id:. See Feature Guide WAF for the full list of rules.
When opening an issue, include:
-
Error logs:
docker compose logs web > web-logs.txt docker compose logs caddy > caddy-logs.txt
-
Environment info:
docker --version docker compose version uname -a
-
Configuration (sanitized):
- Remove secrets (SESSION_SECRET, passwords, API tokens)
- Include docker-compose.yml
- Include relevant .env variables
-
Steps to reproduce
-
Expected vs actual behavior
View logs:
# Follow logs
docker compose logs -f web
docker compose logs -f caddy
# Last 100 lines
docker compose logs --tail=100 web
# Search logs
docker compose logs web | grep -i errorRestart services:
# Restart all
docker compose restart
# Restart specific service
docker compose restart web
docker compose restart caddyContainer shell:
# Web container
docker compose exec web sh
# Caddy container
docker compose exec caddy shCheck configuration:
# Verify docker-compose config
docker compose config
# Check environment variables
docker compose exec web env- Installation Guide - Setup help
- Environment Variables Reference - Configuration reference
- Security Configuration - Security issues
- OAuth Authentication Setup - OAuth troubleshooting
- Certificate Management - Certificate issues
- Rootless Docker Operation - Permission issues
- Feature Guide Forward Auth - Forward auth portal
- Feature Guide Instance Sync - Instance sync and sync key pinning
- Feature Guide WAF - WAF custom directives
Still need help? Open an issue with debug information.