Troubleshooting
Common issues and their solutions when running Patrimoine locally or in production.
Frontend loads but API calls fail
Section titled “Frontend loads but API calls fail”Cause: nginx is not proxying requests correctly to the backend.
Solutions:
- Check that the backend is running and healthy:
podman compose ps— the status must behealthy. - Check
BACKEND_URLinfrontend/container/.env(or the compose defaulthttp://backend:8000): it must be an origin only,http(s)://host[:port], with no path and no trailing slash. Inspect the rendered config withpodman compose exec frontend cat /etc/nginx/conf.d/default.conf. A wrong host answers 502 until the name resolves; seeFRONTEND_BACKEND_RESOLVER. - Check backend logs:
podman compose logs backend.
502 Bad Gateway
Section titled “502 Bad Gateway”Cause: The backend is not ready yet or has crashed.
Solutions:
- Wait a few seconds — the backend may still be starting up (Alembic migration
- uvicorn).
- Check logs:
podman compose logs backend. - Check the healthcheck:
podman compose ps— the backend must reachhealthystatus.
502 Bad Gateway on the OIDC callback
Section titled “502 Bad Gateway on the OIDC callback”Cause: The backend completes the sign-in, but its /auth/callback response carries three token cookies (access, refresh and ID tokens) that do not fit in nginx’s default header buffer. The nginx log shows upstream sent too big header while reading response header from upstream. Larger tokens (extra claims or groups, offline_access) make this more likely.
Solutions:
- Use a frontend image whose nginx configuration sets
proxy_buffer_size 16k;andproxy_busy_buffers_size 16k;. - If you run your own nginx in front of the app, add
proxy_buffer_size 16k;andproxy_busy_buffers_size 16k;to the/auth/location — see Reverse Proxy. - Check the nginx log:
podman compose logs frontend.
OIDC: “Missing PKCE verifier cookie”
Section titled “OIDC: “Missing PKCE verifier cookie””Cause: The pkce_verifier and oauth_state cookies set by /auth/oidc/start
are not being sent back by the browser on the OIDC callback.
Solutions (check each in order):
- SameSite=Lax, not None — Firefox Total Cookie Protection partitions
SameSite=Nonecookies, preventing them from being sent on the callback The app usesSameSite=Laxby default. - nginx
/auth/location — only relevant if you run your own nginx: it must useproxy_http_version 1.1;andproxy_buffering off;. Without these, nginx uses HTTP/1.0 and enables buffering, which can stripSet-Cookieheaders on 302 responses. - Uvicorn
--proxy-headers— the backend must run with--proxy-headersso Uvicorn processesX-Forwarded-ProtoandX-Forwarded-Hostfrom the reverse proxy. This is the default inbackend/container/Containerfile. TRUSTED_PROXY=true(default) — enables readingX-Real-IPfor rate limiting andX-Forwarded-Protofor HTTPS detection.X-Forwarded-Proto: https— the reverse proxy must send this header so the backend sets theSecureflag on cookies.
OIDC: “State mismatch”
Section titled “OIDC: “State mismatch””Cause: Cookies are set by the browser but not sent on the callback request.
Solutions:
- Check the
SameSiteandSecureattributes — if the callback is over HTTPS, cookies must beSecure. If over HTTP, they must not beSecure. - Check that
Path=/is set on the cookies. - Inspect the Firefox Network tab for the
Cookieheader presence on the callback request.
OIDC: Sessions expire quickly (refresh_token empty)
Section titled “OIDC: Sessions expire quickly (refresh_token empty)”Cause: The OIDC provider does not return a refresh_token in the token
exchange response, or returns it empty. When the access token expires
(typically 5–15 minutes for OIDC providers), the backend cannot refresh it,
and the user is logged out.
Symptom: The refresh_token cookie may appear empty or contain """"
in the browser’s cookie inspector. This happens when the OIDC provider returns
an empty string as the refresh_token value — the app now skips setting the
cookie entirely when the value is empty, preventing confusion.
Diagnosis:
- Check backend logs for the warning:
OIDC provider did not return refresh_token — sessions will expire when access_token expires. Ensure the provider grants 'offline_access' scope. - This warning is logged in the
/auth/callbackhandler when the token exchange response lacks arefresh_tokenfield or returns it empty. - Enable
DEBUG=trueinbackend/container/.envto see additional diagnostic logging:OIDC token response keys: [...] — refresh_token present: True/False, length: N
Solutions:
- Grant
offline_accessscope in Authentik — Go to Admin → Applications → Providers → [your provider] → Edit. In Advanced protocol settings, ensureoffline_accessis listed in the selected scopes. Alternatively, create a Scope Mapping foroffline_accessand assign it to the application. - Verify the scope is requested — The app requests
openid profile email offline_accessin/auth/oidc/start(auth.py). This is hardcoded and correct — the issue is always provider-side. - Check provider logs — Authentik logs the token exchange in the
Authorisation flow logs. Verify the
offline_accessscope is present in the granted scopes.
Container won’t start
Section titled “Container won’t start”Cause: Permissions on the ./data directory.
Solutions:
- Verify the directory exists:
ls -la ./data. Create it if missing:mkdir -p ./data. - In rootless mode, Podman maps the container user to your host user. The
./datadirectory must be writable by your user. - Check logs:
podman compose logs backendfor the exact error.
Backend exits at startup with a settings error
Section titled “Backend exits at startup with a settings error”Cause: a setting failed validation; the log line names the variable (for example JWT_SECRET is too short, METRICS_TOKEN is too short, TRUSTED_PROXIES must be a comma-separated list of IP addresses or CIDRs). E2E_TEST_MODE=true without DEBUG=true also stops the backend.
Solutions: fix that variable in backend/container/.env using Environment Variables and restart. Generate secrets with python -c "import secrets; print(secrets.token_urlsafe(48))".
With podman.sh, a PostgreSQL or Redis on a non-default port, or a PostgreSQL cluster of an older major version, is also refused at start.
Scheduled jobs never run
Section titled “Scheduled jobs never run”Cause: REDIS_URL is set but no arq worker runs; while it is set the API leaves the scheduled jobs to the worker.
Solutions: run the worker (Background Worker), or clear REDIS_URL.
/metrics answers 404 or 401
Section titled “/metrics answers 404 or 401”404 means METRICS_TOKEN is empty; 401 means a missing or wrong bearer token. The frontend’s nginx does not proxy /metrics: scrape the backend on the container network (backend:8000). See Metrics.
Migration failures
Section titled “Migration failures”Cause: Corrupt database or inconsistent schema version.
Solutions:
- Check current migration state:
Terminal window podman compose exec backend alembic currentpodman compose exec backend alembic history - If the database is empty or corrupt, you can force a fresh creation: stop
the containers, delete
./data/patrimoine.db, and restart.WARNING: This deletes all data.
Database is locked
Section titled “Database is locked”Cause: SQLite does not handle concurrent access from multiple processes.
This typically happens when sqlite3 commands are run directly on
./data/patrimoine.db while the backend container is running.
Diagnosis:
- Backend logs show:
sqlite3.OperationalError: database is lockedon INSERT/UPDATE statements. - The error may persist after restarting the container because the WAL/SHM
files were created by the external
sqlite3process with different permissions.
Solutions:
- Restart the backend —
podman compose restart backend. This forces SQLAlchemy to create fresh connections. - Fix file permissions — if the container user (UID 10001) can’t read
the database files after a direct
sqlite3access:Terminal window chmod 666 data/patrimoine.db data/patrimoine.db-shm data/patrimoine.db-walpodman compose restart backend - Disable automatic backup — the internal backup task can also cause
lock contention. Set
AUTO_BACKUP=falseinbackend/container/.envand restart. - Prevent recurrence — never run
sqlite3directly on the database while the container is running. Usepodman compose exec backend sqlite3instead, or stop the backend first:Terminal window podman compose stop backendsqlite3 data/patrimoine.db "..."podman compose start backend
ADMIN_EMAILS has no effect
Section titled “ADMIN_EMAILS has no effect”Cause: ADMIN_EMAILS and ADMIN_SUBS only promote accounts that signed in through your OIDC provider, which has verified their email. A local-auth account (email and password registered in Patrimoine) is never promoted, whatever these variables contain — otherwise anyone could claim admin by registering with the configured address before its owner did. For the same reason, a local:<email> value in ADMIN_SUBS is ignored. Email matching is case-insensitive; ADMIN_SUBS must match the sub claim exactly.
Solutions (local-auth instance):
-
Register the first account — when no account exists yet (deleted accounts do not count), the first account registered becomes admin automatically. Registration must be open (
REGISTRATION_ENABLED=true); close it again afterwards. -
Ask an existing admin — open the Administration entry in the profile menu, go to the Users page and tick the Admin checkbox on the user’s row. See User management.
-
Last resort: edit the database — when accounts already exist but none is an admin (for example after updating from a release without the admin role). Back up the database first, then set
is_adminon that account’s row in theuserstable:- SQLite (the booleans are stored as
0/1):Terminal window podman compose exec backend sqlite3 /data/patrimoine.db \"UPDATE users SET is_admin = 1 WHERE email = 'you@example.com' AND auth_provider = 'local' AND deleted_at IS NULL;" - PostgreSQL:
Terminal window docker compose -f docker-compose.yml -f docker-compose.postgres.yml exec postgres \psql -U patrimoine patrimoine -c "UPDATE users SET is_admin = true WHERE email = 'you@example.com' AND auth_provider = 'local' AND deleted_at IS NULL;"
Use the account’s email in lowercase. The role is read from the database on every request and is not stored in the session, so no restart is needed: sign out and back in (or reload the page) and the Administration entry appears.
- SQLite (the booleans are stored as
OIDC instance: check that the email in ADMIN_EMAILS (or the sub in ADMIN_SUBS) matches what your provider sends, then restart the backend or sign in again.
Cookie not sent on callback
Section titled “Cookie not sent on callback”Cause: Browser is rejecting or partitioning the cookie.
Solutions:
- Open Firefox DevTools → Network tab. Check the callback request for a
Cookieheader. If absent, the browser did not send the cookies. - Open Firefox DevTools → Storage tab. Inspect the cookie attributes:
Path=/SameSite=LaxSecure=true(HTTPS only)HttpOnly=true
Log inspection
Section titled “Log inspection”| Command | Purpose |
|---|---|
podman compose logs backend |
Backend (FastAPI/uvicorn) logs |
podman compose logs frontend |
Frontend (nginx) logs |
./podman.sh logs [backend|frontend|docs|postgres|redis|worker] |
Logs of a podman.sh deployment |
See Also
Section titled “See Also”- Cookies & CSRF
- Reverse Proxy
- Backup & Restore — Internal automatic backup, AUTO_BACKUP env var