Troubleshooting
Solutions to common Penombre issues.
Startup
Database not ready after maximum retries
The app retries the database connection 10 times (2 seconds apart) before giving up.
Common causes:
- On SQLite (the default): the directory holding the database file isn't
writable by the container's
bunuser, or the/datavolume isn't mounted. - On PostgreSQL: the server isn't running or hasn't finished starting.
DATABASE_URLis incorrect (wrong path, or wrong host/port/credentials).- A firewall or network issue is blocking the connection (PostgreSQL only).
Verify, on SQLite:
docker compose exec app ls -l /data/dbVerify, on PostgreSQL:
docker compose exec db pg_isreadyIf you added a db service, the app should wait for its health check — make
sure the depends_on condition is set to service_healthy.
Could not migrate the database. Exiting
Migrations retry 10 times (3 seconds apart) before the process exits. Docker's
restart: unless-stopped policy will restart the container automatically.
Common causes:
- Another process holds a lock on the database.
- The database user lacks CREATE/ALTER permissions.
Check migration status:
docker compose logs app | grep -i migrationContainer keeps restarting
Check the logs for the root cause:
docker compose logs app --tail 50The most common reasons are a missing DATABASE_URL, an unreachable database,
or an invalid AUTH_SECRET.
Files are sealed with key ..., but ENCRYPTION_KEY is not set
The instance has sealed files with a key, and this boot has none. Set
ENCRYPTION_KEY (or ENCRYPTION_KEY_FILE) back to that key. See
Encryption.
ENCRYPTION_KEY (id ...) is not the key files are sealed with
The key changed. If that was a rotation, put the old key in
ENCRYPTION_KEY_PREVIOUS and restart; otherwise restore the original key.
ENCRYPTION_KEY is not supported with SIMPLE_MODE=true
Simple mode's root is shared with other tools, so it is never sealed. Remove the key and protect the disk instead.
Previews or durations fail with "sealed with key ..., not loaded"
The worker does not have the key the app has. An external worker needs the same
ENCRYPTION_KEY (or ENCRYPTION_KEY_FILE) and ENCRYPTION_KEY_PREVIOUS; see
Worker.
Authentication
The setup screen keeps appearing, or never appears
Every URL redirects to /auth/setup while the database holds no accounts;
as soon as one exists the screen redirects to sign-in instead. So a setup screen
that will not go away means the account was not created — check the app logs for
the failure — and a missing one means an account already exists.
There are no default credentials to fall back on. If you have lost access to the only account, reset its password through the forgot-password flow (requires SMTP), or connect to the database directly:
# SQLite (default)
docker compose exec app bun -e "console.log([...new (require('bun:sqlite').Database)('/data/db/penombre.sqlite').query('select email from user').all()])"
# PostgreSQL
docker compose exec db psql -U postgres -d penombreOAuth redirect mismatch / callback error
When registering your OAuth application with the provider, the redirect URI must match exactly:
https://<your-domain>/api/v1/auth/callback/<provider-name>The <provider-name> is the lowercase, hyphenated version of the env var
prefix. For OAUTH_POCKET_ID_*, use pocket-id:
https://cloud.example.com/api/v1/auth/callback/pocket-idAlso make sure ORIGIN matches your actual public URL — it's used to build the
callback URL.
Rate limited (HTTP 429)
In production, auth endpoints are rate-limited to 100 requests per 15 minutes per IP. API keys have a separate limit of 100 requests per minute per key.
Wait for the window to reset, or check if a misconfigured client is sending
excessive requests. Rate limiting is disabled when APP_ENV=dev.
Passkeys stopped working after domain change
Passkeys (WebAuthn) are bound to the origin domain. If you change ORIGIN, all
existing passkeys become invalid. Users will need to sign in with another method
and register new passkeys from the settings page.
OAuth error: "At least one OAuth provider must be enabled"
You set ENABLE_OAUTH_SIGNIN=true but no providers are fully configured. Each
provider needs at minimum:
OAUTH_<NAME>_CLIENT_ID=...
OAUTH_<NAME>_CLIENT_SECRET=...
OAUTH_<NAME>_DISCOVERY_URL=...If any of these are missing, the provider is skipped with a warning. Either
complete the configuration or set ENABLE_OAUTH_SIGNIN=false.
Email & SMTP
Email verification / password reset not working
These features require SMTP. If SMTP_ENABLED is false (the default), email
verification is skipped on signup and password reset is unavailable.
Enable it and provide all required fields:
SMTP_ENABLED=true
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user@example.com
SMTP_PASSWORD=app-password
SMTP_FROM=noreply@example.com
SMTP_SECURE=falseUse SMTP_SECURE=true for port 465 (implicit TLS) and false for port 587
(STARTTLS).
SMTP connection refused / authentication failed
- Verify your SMTP host and port are correct.
- Many email providers (Gmail, Outlook) require an app-specific password, not your regular password.
- Check if your server's IP is blocked by the mail provider.
- Enable debug logging (
LOG_LEVEL=debug) and check the output for connection details.
File storage
Uploads fail with permission errors
The /data directory inside the container must be writable by the bun user.
If you're using a bind mount instead of a Docker volume, make sure the host
directory has the correct permissions:
# Check current permissions
docker compose exec app ls -la /data
# Fix from the host (if using a bind mount)
sudo chown -R 1000:1000 /path/to/dataDisk full / uploads fail silently
Check available space on the volume hosting /data:
docker compose exec app df -h /dataClean up trashed files or move the storage volume to a larger disk.
Video or PDF thumbnails not generating
Thumbnail generation requires ffmpeg (video) and poppler-utils (PDF), both
included in the official Docker image. If you're running outside Docker, install
them:
# Debian/Ubuntu
sudo apt install ffmpeg poppler-utils
# macOS
brew install ffmpeg popplerThumbnail failures are logged as warnings but don't block the upload. The file is still stored successfully.
Networking & reverse proxy
ORIGIN mismatch errors
ORIGIN must match the exact public URL users access (including scheme and port
if non-standard):
# Correct
ORIGIN=https://cloud.example.com
# Wrong — trailing slash
ORIGIN=https://cloud.example.com/
# Wrong — HTTP when behind HTTPS proxy
ORIGIN=http://cloud.example.comA mismatch shows up as a form that appears to do nothing — sign-in, setup or a
settings save — with a toast reading Cross-site POST form submissions are
forbidden and a 403 on the request in your browser's console.
Large uploads fail behind a reverse proxy
Penombre sets BODY_SIZE_LIMIT=Infinity, but your reverse proxy may enforce its
own limit.
Nginx — set client_max_body_size 0; to disable the limit:
location / {
client_max_body_size 0;
proxy_pass http://localhost:3000;
}Caddy — has no default body size limit; no change needed.
Traefik — increase the buffering middleware limit or disable it.
502 Bad Gateway / connection refused
The reverse proxy can't reach the Penombre container. Check that:
- The app container is running:
docker compose ps - The container is healthy:
docker inspect --format='{{.State.Health.Status}}' <container> - The proxy and app are on the same Docker network if both are in containers.
- Port 3000 is correct (the default).
Debugging
Enable verbose logging to diagnose issues:
LOG_LEVEL=debug
LOG_FORMAT=jsonView live logs:
docker compose logs -f appNote — log format
jsonproduces structured output that's easier to filter with tools likejq. Useconsolefor human-readable output during development.
Check the database directly (PostgreSQL):
docker compose exec db psql -U postgres -d penombre -c "\dt"Check the health endpoint:
curl http://localhost:3000/api/healthThis guide lives in the project repo: edit it there, and this page follows within a day.