* docs: complete env reference; troubleshooting keyed on real error messages
Configuration (docs/5-CONFIGURATION):
- environment-reference.md is now the single complete list, checked
against the code and the content-core, esperanto, surreal-commands and
langsmith libraries: adds the undocumented variables, a "Read by"
column, correct defaults (SURREAL_COMMANDS_RETRY_ENABLED=false,
WAIT_MAX=60, LANGCHAIN_PROJECT=default, SurrealDB defaults), which
legacy provider vars Migrate to Database copies, and a table of
variables nothing reads.
- security.md: PBKDF2 derivation, POST /api/credentials/migrate-encryption
with the back-up-first / no-downgrade warning, password stored in
localStorage, full list of unauthenticated paths, real upload endpoint.
- reverse-proxy.md: drop v1.0 legacy sections and the non-existent
multi-container images; real /api/config output; how API_URL
auto-detection works; the 100 MB Next.js proxy cap.
- ai-providers.md, ollama.md, openai-compatible.md: one shared setup
procedure with current UI labels, provider/modality table from
provider_registry.py, no stale model lists or prices; Ollama
num_ctx and 180 s ESPERANTO_LLM_TIMEOUT; Azure form limits.
- local-tts.md is the shared Speaches page; local-stt.md keeps only
STT specifics (Whisper models, CCORE_AUDIO_SEGMENT_MINUTES).
- advanced.md, index.md, database.md: link to the reference instead of
re-listing variables; fix service names, port changes, backups.
Troubleshooting (docs/6-TROUBLESHOOTING):
- Entries start with the exact message from the code or the en-US
locale. New processing-issues.md covers queued/failed sources (the
content-core failure reasons), uploads, search, podcasts (profile
errors and NOTE hints). ai-chat-issues.md covers timeouts, error
classifier messages, empty responses, Ollama context, encryption
key and decryption errors.
- Removed invented entries ("too many connections", "Model is not a
LanguageModel: None", soft delete, retry vars as a connection fix).
SECURITY_REVIEW.md is marked as a historical January 2026 snapshot of
the credentials UI, with the SSRF and security-warning rows corrected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: address review on configuration and troubleshooting pages
- No predictable keys or passwords: placeholders plus the openssl /
PowerShell generation commands used by the install docs.
- Every 0.0.0.0 bind and firewall rule (Ollama, OpenAI-compatible
servers, Speaches, LAN access) restricted to the host and Docker
networks or the LAN, with an explicit exposure warning.
- Diagnostic commands no longer print secrets.
- systemd drop-in includes [Service]; Traefik middleware attached to
the router.
- Provider pages link to Connect a provider (#1459) instead of
repeating steps; Vertex and keyless local providers called out;
"Add Selected" (a select-all toggle) no longer described as the add
button; "Manage -> Models" instead of "the Models page".
- Startup database failure described correctly (API down, not the
Database Connection Failed overlay); worker crash causes, Test
timeout/404 meanings, .env overrides when moving servers, frontend
restart from source, privacy claims scoped to self-hosted servers.
- Drop claims about bugs fixed in #1457; restore the ollama.md
GPU acceleration anchor used by quick-start-local.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: address second review round on configuration pages
- Anthropic Compatible always needs an API key and a Base URL
(enforced in api/models.py), noted in the provider differences.
- Speaches firewall guidance covers a separate Open Notebook host.
- The password check mirrors get_secret_from_env: a readable,
non-empty _FILE wins, otherwise the variable; never prints values.
- From source, frontend variables go in frontend/.env.local or the
shell: `make frontend` runs `next dev` in frontend/, which doesn't
read the root .env.
- The env listing prints every variable name, without values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: correct the default API path for the frontend; deduplicate
Without API_URL the browser calls <host>:5055 directly (the /config
route auto-detects it); INTERNAL_API_URL only serves requests that hit
the Next.js /api forwarding. Full explanation stays in the environment
reference; the configuration index links to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: upload limit depends on whether API calls go through the frontend proxy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
Reverse Proxy Configuration
Run Open Notebook behind nginx, Caddy, Traefik or a hosting platform, with your own domain and HTTPS.
How It Works
The lfnovo/open_notebook image runs three processes in one container: the Next.js frontend on port 8502, the API on port 5055 and the background worker. The Next.js server forwards every /api/* request it receives to the API (INTERNAL_API_URL, default http://localhost:5055).
So a reverse proxy only needs to reach port 8502:
Browser ──HTTPS──> Reverse proxy ──> :8502 Next.js ──/api/*──> :5055 API
For this to work, the browser must send its API calls to your public URL too. That is what API_URL does.
How the browser finds the API
When the UI loads, it asks the Next.js server (GET /config) where the API is:
- If
API_URLis set, that value is used. (NEXT_PUBLIC_API_URLis accepted as an older name.) - Otherwise the server builds it from the request: the
X-Forwarded-Protoheader (or the request's own scheme), the hostname from theHostheader, and port 5055. A page loaded fromhttp://192.168.1.50:8502calls the API athttp://192.168.1.50:5055.
Auto-detection is fine when the browser can reach port 5055 directly (localhost, a LAN). Behind a reverse proxy it produces https://notebook.example.com:5055, which usually doesn't exist. Behind a reverse proxy, set API_URL to your public URL, without /api:
services:
open_notebook:
environment:
- API_URL=https://notebook.example.com
Apply it with docker compose up -d. When auto-detection is used, the container log shows [runtime-config] Auto-detected API URL: ..., which tells you what the browser was given.
All variables mentioned here are listed in the Environment Reference.
Nginx
server {
listen 443 ssl http2;
server_name notebook.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# Uploads: match OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB (default 100)
client_max_body_size 100M;
location / {
proxy_pass http://open_notebook:8502;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
# Long-running requests (chat with slow models, transformations)
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
server {
listen 80;
server_name notebook.example.com;
return 301 https://$server_name$request_uri;
}
open_notebook is the compose service name; use 127.0.0.1 if nginx runs on the host instead of in the same compose project.
Compose example with nginx
Add an nginx service next to the surrealdb and open_notebook services of the shipped docker-compose.yml, and stop publishing the app ports to the outside:
services:
open_notebook:
image: lfnovo/open_notebook:v1-latest
environment:
- API_URL=https://notebook.example.com
- OPEN_NOTEBOOK_ENCRYPTION_KEY=<generated-key>
- OPEN_NOTEBOOK_PASSWORD=<generated-password>
# plus the SURREAL_* variables from the shipped file
ports:
- "127.0.0.1:8502:8502" # local access only; nginx uses the compose network
volumes:
- ./notebook_data:/app/data
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- open_notebook
Replace <generated-key> and <generated-password> with values you generate yourself, for example with openssl rand -hex 32 (on Windows, see Set your encryption key). Don't reuse an example value.
Caddy
notebook.example.com {
request_body {
max_size 100MB
}
reverse_proxy open_notebook:8502 {
transport http {
read_timeout 600s
write_timeout 600s
}
}
}
Caddy obtains and renews certificates automatically.
Traefik
services:
open_notebook:
image: lfnovo/open_notebook:v1-latest
environment:
- API_URL=https://notebook.example.com
# plus the variables from the shipped docker-compose.yml
labels:
- "traefik.enable=true"
- "traefik.http.routers.notebook.rule=Host(`notebook.example.com`)"
- "traefik.http.routers.notebook.entrypoints=websecure"
- "traefik.http.routers.notebook.tls.certresolver=myresolver"
- "traefik.http.services.notebook.loadbalancer.server.port=8502"
networks:
- traefik-network
Raise the response timeout in Traefik's static configuration so long requests aren't cut off:
# traefik.yml
serversTransport:
forwardingTimeouts:
responseHeaderTimeout: 600s
Coolify and similar platforms
- Deploy the Docker Compose setup.
- Point the platform's domain at port 8502 of the
open_notebookservice. - Add
API_URL=https://your-domain.comto the service's environment. - Enable HTTPS in the platform.
Exposing the API to other clients
Browser traffic only needs port 8502. Scripts and integrations can use the same public URL (https://notebook.example.com/api/...), which goes through Next.js. To route API calls straight to the API instead, add a location for /api/:
location /api/ {
proxy_pass http://open_notebook:5055/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600s;
}
location / {
proxy_pass http://open_notebook:8502;
# ... same headers as above
}
Routing /api/ directly also avoids the 100 MB upload cap of the Next.js proxy (see Upload size).
API on its own subdomain
Serve the UI at notebook.example.com and the API at api.notebook.example.com: proxy the first to port 8502 and the second to port 5055, and set API_URL=https://api.notebook.example.com. The browser then calls a different origin, so if you restrict CORS_ORIGINS, include https://notebook.example.com.
Access from other machines without a proxy
On a LAN you can skip the proxy. Publish both ports (the shipped compose file does) and open http://<server-ip>:8502. Auto-detection sends API calls to http://<server-ip>:5055, so port 5055 must be reachable from the client:
# On the server, if a firewall is active: allow only your LAN (adjust the subnet)
sudo ufw allow from 192.168.1.0/24 to any port 8502 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 5055 proto tcp
Without a reverse proxy, traffic is plain HTTP, including the password header. Only do this on a network you trust, set OPEN_NOTEBOOK_PASSWORD, and never open these ports to the internet. Docker publishes ports through its own iptables rules, which can bypass UFW; to keep a port private, bind it to 127.0.0.1 in docker-compose.yml instead.
If you changed the host side of the API port mapping (for example "5056:5055"), auto-detection still assumes 5055: set API_URL=http://<server-ip>:5056.
Timeouts
The web UI waits up to 10 minutes for an API response, and each model call is limited by ESPERANTO_LLM_TIMEOUT (180 seconds by default). Set proxy read timeouts to at least 600 seconds so the proxy is never the first to give up. Common proxy defaults (60 seconds for nginx) cut off slow chat answers and transformations with 502/504 errors or socket hang up in the browser.
For slow models, raise ESPERANTO_LLM_TIMEOUT (keep it below 600). See Environment Reference → Timeouts.
Upload size (413 errors)
Three limits apply to uploads from the UI, and the smallest wins:
| Limit | Default | Where to change it |
|---|---|---|
| Your reverse proxy | nginx: 1 MB | client_max_body_size, Caddy request_body max_size, Traefik buffering middleware |
Next.js proxy for /api/* |
100 MB | Fixed in the frontend build |
| API | 100 MB | OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB |
When the proxy rejects a file, the response doesn't come from Open Notebook, so it has no CORS headers and the browser reports a CORS error with status 413. When the API rejects it, the message is Request body exceeds the maximum allowed upload size. To upload files larger than 100 MB, raise OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB and send them to the API directly (port 5055, or a /api/ location routed to 5055 as above).
Traefik example (dynamic file configuration):
http:
middlewares:
large-body:
buffering:
maxRequestBodyBytes: 104857600 # 100 MB
A middleware does nothing until a router uses it. Attach it to the router from the Traefik example:
labels:
- "traefik.http.routers.notebook.middlewares=large-body@file"
(Or define it with labels too: traefik.http.middlewares.large-body.buffering.maxRequestBodyBytes=104857600 plus traefik.http.routers.notebook.middlewares=large-body.)
Kubernetes ingress-nginx: annotation nginx.ingress.kubernetes.io/proxy-body-size: "100m".
Troubleshooting
"Unable to Connect to API Server"
The UI loaded but its first API call failed. The overlay's Show Technical Details shows the Attempted URL.
- Attempted URL ends in
:5055behind a proxy →API_URLisn't set (or the container wasn't recreated after setting it). Check withdocker compose exec open_notebook printenv API_URL, thendocker compose up -d. - Attempted URL is
http://...on an HTTPS page → the browser blocks it as mixed content. SetAPI_URLwithhttps://, or make sure the proxy sendsX-Forwarded-Proto. - Attempted URL looks right → test it:
curl https://notebook.example.com/api/config. A healthy instance answers without a password:
{"version": "1.15.0", "latestVersion": "1.15.0", "hasUpdate": false, "dbStatus": "online"}
If that fails, the proxy isn't reaching port 8502, or the container isn't running (docker compose ps, docker compose logs open_notebook).
"Database Connection Failed"
The proxy and API are fine; the API can't reach SurrealDB. See Connection Issues.
502 Bad Gateway
- The container isn't running or is still starting:
docker compose ps,docker compose logs open_notebook. - The proxy can't resolve
open_notebook: it must be on the same Docker network, or use127.0.0.1:8502from the host.
CORS errors in the browser console
- Status 413 alongside the CORS error → an upload hit a size limit; see Upload size.
- You set
CORS_ORIGINS→ the origin in your address bar must be in it, exactly (scheme, host and port). API_URLpoints at a different origin than the page (another port or subdomain) and the request fails for another reason (502, timeout) → the error page from the proxy has no CORS headers. Fix the underlying error.
{"detail": "Missing authorization header"}
OPEN_NOTEBOOK_PASSWORD is set and the request had no Authorization: Bearer <password> header. Expected when you open an API URL such as /api/notebooks in the browser. Log in through the UI, or send the header from scripts. (/api/config and /health never require it.)
Browser console logs
The [Config] messages in the browser console only appear in development builds (npm run dev). On the Docker image, use the container log line [runtime-config] Auto-detected API URL and the Attempted URL in the error overlay instead.
Checklist
- HTTPS in front, app ports bound to
127.0.0.1or not published. API_URLset to the public URL (no/api), applied withdocker compose up -d.- Proxy timeouts of at least 600 seconds.
- Proxy body limit matching
OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB. OPEN_NOTEBOOK_PASSWORDset, andCORS_ORIGINSset to your domain.- Secrets kept out of version control: put them in an untracked
docker-compose.override.yml(seedocker-compose.override.yml.example) or an uncommittedenv_file:.
Related
- Security Configuration — password, CORS, hardening
- Environment Reference — all variables
- Connection Issues — when the UI can't reach the API
- Docker Compose installation