Skip to content

Troubleshooting

A running list of the failure modes that actually catch self-hosters out. Organised as a Q&A. If your symptom is not here, turn on LOG_LEVEL=debug in the container and check the server logs first; verbose logs surface most of these cases explicitly.

Login and cookies

Signing in over plain HTTP sends you back to the sign-in page

Symptom: You open the app at a plain http:// address (a LAN IP, an Unraid WebUI link), the sign-in form takes your password, and you're back on the sign-in page. NutriTrace 1.4.0, NoteTrace 1.1.0 and LiftTrace 1.4.0 and later say Signing in won't work on this address on the sign-in and setup screens, and log Sign-in from a plain-HTTP page; CookTrace from its next release.

Cause: The sign-in cookie is marked Secure by default, so browsers keep it only on HTTPS pages and http://localhost. Over any other plain http:// address the browser silently throws it away, so the session never sticks. Firefox's console names the rejected cookie.

Fix: Add INSECURE_COOKIES=1 to the container environment and restart. Only do this on a trusted LAN; the variable is named the way it is for a reason. The better long-term answer is real TLS. See Plain-HTTP LAN, and HTTPS on the LAN for how to set that up with Let's Encrypt, a Cloudflare Tunnel, or a self-signed CA installed on your devices.

Signing in over HTTPS sends you back to the sign-in page

Symptom: The page is https:// and your password is right, but you're back on the sign-in page. NutriTrace 1.4.0, NoteTrace 1.1.0 and LiftTrace 1.4.0 and later say Your browser didn't keep you signed in; CookTrace from its next release.

Cause and fix: Something between the sign-in and the next request dropped the cookie: blocked cookies, a frame on another site, or a proxy that changes it. See If signing in doesn't stick.

Android release APK will not connect to my LAN server

Symptom: The web PWA loads fine from your laptop, but the release-signed Android APK refuses to connect. The error looks like a network timeout or an SSL handshake failure.

Cause: Android's release-build network security config bans cleartext (http://) traffic outright. This is enforced by the OS, not the app.

Fix: Use HTTPS. The four supported paths:

  • Real domain plus Let's Encrypt (via Caddy, Traefik, or nginx with certbot).
  • Cloudflare Tunnel or Tailscale Funnel (both give you a public cert without opening ports).
  • A self-signed CA installed on the Android device's trust store.
  • Build the debug APK yourself with npm run android:debug in the app repo; the debug variant is allowed to talk to http:// origins.

Details in HTTPS on the LAN and Cloudflare Tunnel and Tailscale.

Trace AI

Trace returns "model not found" or a Gemini 404

Symptom: Trace works with some providers but Gemini calls fail with 404, or the UI shows a "model not found" error after an upgrade.

Cause: Google retires Gemini model IDs regularly. The saved user setting still points at a retired one (gemini-1.5-flash, gemini-1.5-pro, gemini-2.0-flash, gemini-2.0-flash-lite). The server keeps a GEMINI_RETIRED set and auto-remaps to the current default (gemini-2.5-flash) on next call, but the remap runs once at boot; a saved user preference can still slip through.

Fix: Restart the container to trigger the remap, then open Settings > Trace AI and either pick a fresh model from the dropdown or clear the saved setting. If you want to override with a model that is not on the built-in list, use the __custom__ option (AI_MODEL_CUSTOM on CookTrace) and type the ID by hand. See Model list and Gemini retirement remap.

Backups

Full-backup restore fails behind a reverse proxy

Symptom, cause, and the two proxy caps to check

Symptom: The full-backup ZIP downloads fine but the restore upload fails part-way through, often with a 413 or a truncated request.

Cause: The reverse proxy in front of your instance is capping the request body below the backup size.

Fix: Two caps to check:

  • Cloudflare free tier proxies bodies at up to 100 MB. Restore uploads larger than that will not fit. Bypass Cloudflare for the restore (Tailscale, a direct LAN address, or temporarily "grey-cloud" the DNS record), or pay for a plan with a higher cap.
  • nginx defaults client_max_body_size to 1 MB. Bump it explicitly in your server block: client_max_body_size 2048m; or whatever comfortably exceeds your backup size. Caddy and Traefik have generous defaults but check your specific configuration.

See Backups and restore and Reverse-proxy recipes for full snippets.

OIDC / SSO

SSO sign-in ends on a blank page

Symptom: You sign in at your IdP fine, it redirects back, and the app shows a blank white page. The container log shows requests like GET /api/oidc/callback or GET /api/oidc/assets/....

Cause: The callback URL is wrong. The IdP sent you back to a path the app doesn't handle. CookTrace handles only the path below; NutriTrace 1.4.0, NoteTrace 1.1.0 and LiftTrace 1.4.0 and later also accept it without the number, and at /api/oidc/callback.

Fix: Use https://<your-host>/api/auth/oidc/callback/<provider-id> (the ID is in the startup log line [oidc-env] Loaded 1 OIDC provider from environment (IDs: 1), usually 1), in both your IdP and OIDC_REDIRECT_URIS, then docker compose up -d. See The callback URL.

SSO sign-in works but you're "not authenticated" right after

Symptom: Sign-in (SSO or password) appears to succeed, then every page says you're not signed in.

Cause: You're reaching the app over plain http:// on an address other than localhost. The sign-in cookie is marked Secure, and browsers drop Secure cookies on plain HTTP. Over HTTPS, see Signing in over HTTPS sends you back.

Fix: Use HTTPS (a reverse proxy), or set INSECURE_COOKIES=1 on a trusted LAN. See Plain-HTTP LAN.

OIDC login works but the user has no admin rights

Symptom: Users authenticate through your IdP fine, get logged in, but land as regular users even though they are in the group that is supposed to grant admin.

Cause: The group claim in the ID token doesn't match what the app is looking for. Two things have to line up: the claim name (OIDC_ADMIN_GROUP_CLAIM) and the value inside it (OIDC_ADMIN_GROUP_VALUE). There is no default; both must be set. The claim also has to be in the ID token: the app doesn't read the userinfo endpoint.

Fix: Decode the ID token your IdP issues (the IdP's admin console usually has a preview, or paste it into a JWT decoder) and confirm the exact claim name and value. Update the two env vars to match, run docker compose up -d, and have the user sign out and back in. The role is set at each sign-in, so an existing session won't pick up the change until the next one. Per-IdP walkthroughs live under OIDC recipes.

SSO stops working after rotating JWT_SECRET

Symptom: After changing JWT_SECRET, everyone is signed out (expected), and SSO sign-in fails for providers you added in Settings → Authentication.

Cause: Unless TOKEN_ENC_KEY is set, the key that encrypts stored OIDC client secrets is derived from JWT_SECRET. A new JWT_SECRET means the stored secrets can no longer be decrypted. Providers defined through env vars are unaffected: their secret is re-read from the environment at every start.

Fix: Re-enter the client secret of each provider in Settings → Authentication. To be able to rotate JWT_SECRET later without this, set TOKEN_ENC_KEY to a stable value and re-enter the secrets once more (setting it changes the key too); after that, JWT_SECRET can change freely. See Session lifetime and password policy.

NutriTrace-specific

Open Food Facts (OFF) local mirror errors with EISDIR

Symptom: On NutriTrace, the OFF mirror refresh fails with an EISDIR: illegal operation on a directory error, and food lookups against the local mirror return empty.

Cause: OFF_LOCAL_DB must point at a file path, not a directory. If the compose file bind-mounts a host directory onto the path in OFF_LOCAL_DB, Docker auto-creates a directory at that path on first start, and every subsequent file open fails.

Fix: In your compose file, mount the containing directory (something like ./off-mirror:/data/off-mirror) and set OFF_LOCAL_DB=/data/off-mirror/off.duckdb to point at the file inside it. Do not set OFF_LOCAL_DB to the mount point itself. See Open Food Facts.

Diary items disappear after force-killing the app overnight

Symptom: You log a few meals on the NutriTrace Android app in the evening, force-kill the app or the OS kills it in the background, and by morning the meals are gone. Water entries logged the same evening are still there.

Cause: On older NT Android builds, diary item writes were debounced to SQLite while water writes flushed immediately. If the app process died before the debounce timer fired, the diary rows never made it out of the JavaScript layer into SQLite storage. Water was unaffected because it took a different code path.

Fix: Upgrade to NutriTrace 1.0.3 (Android versionCode 128) or newer. Diary writes now flush on pause, visibilitychange, and pagehide events, matching the water write path. If you are already on a current build, confirm the version in Settings > About and re-check that the affected day is still absent from the server after a manual sync (from your PWA at, for example, https://nutritrace.example.com); a genuinely-missing diary should also be missing there.

When nothing else helps

  • Reproduce with LOG_LEVEL=debug in the server container and grab the last 200 lines.
  • On the phone, turn on Settings > Diagnostics > Verbose and copy the diagnostic log.
  • Open an issue on the relevant repo (TraceApps/cooktrace, TraceApps/lifttrace, TraceApps/nutritrace) with the version, the log snippet, and the exact steps to reproduce. Redact anything sensitive; wellness logs in particular contain PHI (HRV, resting heart rate, sleep, calories).