docs/5-CONFIGURATION/reverse-proxy.md
Deploy Open Notebook behind nginx, Caddy, Traefik, or other reverse proxies with custom domains and HTTPS.
Starting with v1.1, Open Notebook uses Next.js rewrites to simplify configuration. You only need to proxy to one port - Next.js handles internal API routing automatically.
Browser → Reverse Proxy → Port 8502 (Next.js)
↓ (internal proxy)
Port 5055 (FastAPI)
Next.js automatically forwards /api/* requests to the FastAPI backend, so your reverse proxy only needs one port!
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;
# Allow file uploads up to 100MB
client_max_body_size 100M;
# Single location block - that's it!
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';
proxy_cache_bypass $http_upgrade;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name notebook.example.com;
return 301 https://$server_name$request_uri;
}
notebook.example.com {
reverse_proxy open-notebook:8502 {
transport http {
read_timeout 600s
write_timeout 600s
}
}
}
Caddy handles HTTPS automatically. The timeout settings ensure long-running operations (transformations, podcast generation) don't fail.
# Add this to your docker-compose.yml alongside the surrealdb service
# See full base setup: https://github.com/lfnovo/open-notebook/blob/main/docker-compose.yml
services:
open-notebook:
image: lfnovo/open_notebook:v1-latest
pull_policy: always
environment:
- API_URL=https://notebook.example.com
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"
# Timeout for long-running operations (transformations, podcasts)
- "traefik.http.services.notebook.loadbalancer.responseforwarding.flushinterval=100ms"
networks:
- traefik-network
Note: For Traefik v2+, you may also need to configure serversTransport timeouts in your static configuration:
# traefik.yml (static configuration)
serversTransport:
forwardingTimeouts:
dialTimeout: 30s
responseHeaderTimeout: 600s
idleConnTimeout: 90s
API_URL=https://your-domain.com# Required for reverse proxy setups
API_URL=https://your-domain.com
# Optional: For multi-container deployments
# INTERNAL_API_URL=http://api-service:5055
Important: Set API_URL to your public URL (with https://).
Note on HOSTNAME: The Docker images set HOSTNAME=0.0.0.0 by default, which ensures Next.js binds to all interfaces and is accessible from reverse proxies. You typically don't need to set this manually.
The frontend uses a three-tier priority system to determine the API URL:
API_URL environment variable set at container runtimeNEXT_PUBLIC_API_URL baked into the Docker imageWhen API_URL is not set, the Next.js frontend:
host headerX-Forwarded-Proto header (for HTTPS behind reverse proxies){protocol}://{hostname}:5055http://10.20.30.20:8502 → API URL becomes http://10.20.30.20:5055Why set API_URL explicitly?
https:// when behind SSL-terminating proxyImportant: Don't include /api at the end - the system adds this automatically!
Note: This example only shows the open-notebook and nginx services. You also need a
surrealdbservice. See the full base docker-compose.yml for the complete setup.
services:
open-notebook:
image: lfnovo/open_notebook:v1-latest
pull_policy: always
container_name: open-notebook
environment:
- API_URL=https://notebook.example.com
- OPEN_NOTEBOOK_ENCRYPTION_KEY=${OPEN_NOTEBOOK_ENCRYPTION_KEY}
- OPEN_NOTEBOOK_PASSWORD=${OPEN_NOTEBOOK_PASSWORD}
volumes:
- ./notebook_data:/app/data
# Only expose to localhost (nginx handles public access)
ports:
- "127.0.0.1:8502:8502"
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- open-notebook
restart: unless-stopped
events {
worker_connections 1024;
}
http {
upstream notebook {
server open-notebook:8502;
}
# HTTP redirect
server {
listen 80;
server_name notebook.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS server
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;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Allow file uploads up to 100MB
client_max_body_size 100M;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
# Proxy settings
location / {
proxy_pass http://notebook;
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';
proxy_cache_bypass $http_upgrade;
# Timeouts for long-running operations (transformations, podcasts, etc.)
# 600s matches the frontend timeout for slow LLM operations
proxy_read_timeout 600s;
proxy_connect_timeout 60s;
proxy_send_timeout 600s;
}
}
}
If external scripts or integrations need direct API access, route /api/* directly:
# Direct API access (for external integrations)
location /api/ {
proxy_pass http://open-notebook:5055/api/;
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;
}
# Frontend (handles all other traffic)
location / {
proxy_pass http://open-notebook:8502;
# ... same headers as above
}
Note: This is only needed for external API integrations. Browser traffic works fine with single-port setup.
Accessing Open Notebook from a different machine on your network:
Step 1: Get your server IP
# On the server running Open Notebook:
hostname -I
# or
ifconfig | grep "inet "
# Note the IP (e.g., 192.168.1.100)
Step 2: Configure API_URL
# In docker-compose.yml or .env:
API_URL=http://192.168.1.100:5055
Step 3: Expose ports
# Add to your docker-compose.yml (requires surrealdb service, see installation guide)
services:
open-notebook:
image: lfnovo/open_notebook:v1-latest
pull_policy: always
environment:
- API_URL=http://192.168.1.100:5055
ports:
- "8502:8502"
- "5055:5055"
Step 4: Access from client machine
# In browser on other machine:
http://192.168.1.100:8502
Troubleshooting:
sudo ufw allow 8502 && sudo ufw allow 5055ping 192.168.1.100 from client machinetelnet 192.168.1.100 8502 from client machineHost the API and frontend on different subdomains:
docker-compose.yml:
# Add to your docker-compose.yml (requires surrealdb service, see installation guide)
services:
open-notebook:
image: lfnovo/open_notebook:v1-latest
pull_policy: always
environment:
- API_URL=https://api.notebook.example.com
- OPEN_NOTEBOOK_ENCRYPTION_KEY=${OPEN_NOTEBOOK_ENCRYPTION_KEY}
# Don't expose ports (nginx handles routing)
nginx.conf:
# Frontend server
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;
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';
proxy_cache_bypass $http_upgrade;
}
}
# API server (separate subdomain)
server {
listen 443 ssl http2;
server_name api.notebook.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass http://open-notebook:5055;
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;
}
}
Use case: Separate DNS records, different rate limiting, or isolated API access control.
For complex deployments with separate frontend and API containers:
docker-compose.yml:
services:
frontend:
image: lfnovo/open_notebook_frontend:v1-latest
pull_policy: always
environment:
- API_URL=https://notebook.example.com
ports:
- "8502:8502"
api:
image: lfnovo/open_notebook_api:v1-latest
pull_policy: always
environment:
- OPEN_NOTEBOOK_ENCRYPTION_KEY=${OPEN_NOTEBOOK_ENCRYPTION_KEY}
ports:
- "5055:5055"
depends_on:
- surrealdb
surrealdb:
image: surrealdb/surrealdb:latest
command: start --log trace --user root --pass root file:/mydata/database.db
ports:
- "8000:8000"
volumes:
- ./surreal_data:/mydata
nginx.conf:
http {
upstream frontend {
server frontend:8502;
}
upstream api {
server api:5055;
}
server {
listen 443 ssl http2;
server_name notebook.example.com;
# API routes
location /api/ {
proxy_pass http://api/api/;
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;
}
# Frontend (catch-all)
location / {
proxy_pass http://frontend;
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';
proxy_cache_bypass $http_upgrade;
}
}
}
Note: Most users should use the Docker Compose approach (v1-latest). Multi-container with separate nginx is only needed for custom scaling or isolation requirements.
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d notebook.example.com
# Auto-renewal (usually configured automatically)
sudo certbot renew --dry-run
Caddy handles SSL automatically - no configuration needed!
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/privkey.pem \
-out ssl/fullchain.pem \
-subj "/CN=localhost"
Check API_URL is set:
docker exec open-notebook env | grep API_URL
Verify reverse proxy reaches container:
curl -I http://localhost:8502
Check browser console (F12):
Frontend using HTTPS but trying to reach HTTP API:
# Ensure API_URL uses https://
API_URL=https://notebook.example.com # Not http://
Ensure your proxy supports WebSocket upgrades:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
docker psdocker logs open-notebookSymptoms:
socket hang up or ECONNRESET errorsTimeout after 30000ms errorsCause: Your reverse proxy has a default timeout (often 30s) that's shorter than Open Notebook's operations.
Solutions by proxy:
Nginx:
proxy_read_timeout 600s;
proxy_send_timeout 600s;
Caddy:
reverse_proxy open-notebook:8502 {
transport http {
read_timeout 600s
write_timeout 600s
}
}
Traefik (static config):
serversTransport:
forwardingTimeouts:
responseHeaderTimeout: 600s
Application-level timeouts:
If you still experience timeouts after configuring your proxy, you can also adjust the application timeouts:
# In .env file:
API_CLIENT_TIMEOUT=600 # API client timeout (default: 300s)
ESPERANTO_LLM_TIMEOUT=180 # LLM inference timeout (default: 60s)
See Advanced Configuration for more timeout options.
Step 1: Check browser console (F12 → Console tab)
Look for messages starting with 🔧 [Config]
These show the configuration detection process
You'll see which API URL is being used
Example good output:
✅ [Config] Runtime API URL from server: https://your-domain.com
Example bad output:
❌ [Config] Failed to fetch runtime config
⚠️ [Config] Using auto-detected URL: http://localhost:5055
Step 2: Test API directly
# Should return JSON config
curl https://your-domain.com/api/config
# Expected output:
{"status":"ok","credentials_configured":true,...}
Step 3: Check Docker logs
docker logs open-notebook
# Look for:
# - Frontend startup: "▲ Next.js ready on http://0.0.0.0:8502"
# - API startup: "INFO: Uvicorn running on http://0.0.0.0:5055"
# - Connection errors or CORS issues
Step 4: Verify environment variable
docker exec open-notebook env | grep API_URL
# Should show:
# API_URL=https://your-domain.com
:5055 to URL (Versions ≤ 1.0.10)Symptoms (only in older versions):
API_URL=https://your-domain.comRoot Cause:
In versions ≤ 1.0.10, the frontend's config endpoint was at /api/runtime-config, which got intercepted by reverse proxies routing all /api/* requests to the backend. This prevented the frontend from reading the API_URL environment variable.
Solution:
Upgrade to version 1.0.11 or later. The config endpoint has been moved to /config which avoids the /api/* routing conflict.
Verification:
Check browser console (F12) - should see: ✅ [Config] Runtime API URL from server: https://your-domain.com
If you can't upgrade, explicitly configure the /config route:
# Only needed for versions ≤ 1.0.10
location = /config {
proxy_pass http://open-notebook:8502;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
Symptoms:
CORS header 'Access-Control-Allow-Origin' missing. Status code: 413.
Error creating source. Please try again.
Root Cause: When uploading files, your reverse proxy may reject the request due to body size limits before it reaches the application. Since the error happens at the proxy level, CORS headers are not included in the response.
Version Requirement:
proxyClientMaxBodySize configuration optionSolutions:
Nginx - Increase body size limit:
server {
# Allow larger file uploads (default is 1MB)
client_max_body_size 100M;
# Add CORS headers to error responses
error_page 413 = @cors_error_413;
location @cors_error_413 {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
return 413 '{"detail": "File too large. Maximum size is 100MB."}';
}
location / {
# ... your existing proxy configuration
}
}
Traefik - Increase buffer size:
# In your traefik configuration
http:
middlewares:
large-body:
buffering:
maxRequestBodyBytes: 104857600 # 100MB
Apply middleware to your router:
labels:
- "traefik.http.routers.notebook.middlewares=large-body"
Kubernetes Ingress (nginx-ingress):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: open-notebook
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
# Add CORS headers for error responses
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "Access-Control-Allow-Origin: *";
Caddy:
notebook.example.com {
request_body {
max_size 100MB
}
reverse_proxy open-notebook:8502 {
transport http {
read_timeout 600s
write_timeout 600s
}
}
}
Note: Open Notebook's API includes CORS headers in error responses, but this only works for errors that reach the application. Proxy-level errors (like 413 from nginx) need to be configured at the proxy level.
Symptoms:
Access-Control-Allow-Origin header is missing
Cross-Origin Request Blocked
Response to preflight request doesn't pass access control check
Possible Causes:
Missing proxy headers:
# Make sure these are set:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
API_URL protocol mismatch:
# Frontend is HTTPS, but API_URL is HTTP:
API_URL=http://notebook.example.com # ❌ Wrong
API_URL=https://notebook.example.com # ✅ Correct
Reverse proxy not forwarding /api/* correctly:
# Make sure this works:
location /api/ {
proxy_pass http://open-notebook:5055/api/; # Note the trailing slash!
}
Symptoms:
{"detail": "Missing authorization header"}
This happens when:
OPEN_NOTEBOOK_PASSWORD for authentication/api/config directly without logging in firstSolution: This is expected behavior! The frontend handles authentication automatically. Just:
/api/ directly)For API integrations: Include the password in the Authorization header:
curl -H "Authorization: Bearer your-password-here" \
https://your-domain.com/api/config
Symptoms:
Solutions:
Use Let's Encrypt (recommended):
sudo certbot --nginx -d notebook.example.com
Check certificate paths in nginx:
ssl_certificate /etc/nginx/ssl/fullchain.pem; # Full chain
ssl_certificate_key /etc/nginx/ssl/privkey.pem; # Private key
Verify certificate is valid:
openssl x509 -in /etc/nginx/ssl/fullchain.pem -text -noout
For development, use self-signed (not for production):
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl/privkey.pem -out ssl/fullchain.pem \
-subj "/CN=localhost"
127.0.0.1:8502) and let proxy handle public access for security.env or docker.env) to manage configuration securelycurl https://your-domain.com/api/configdocker logs open-notebook/api in API_URL - the system adds this automaticallyIf you're running Open Notebook version 1.0.x or earlier, you may need to use the legacy two-port configuration where you explicitly route /api/* to port 5055.
Check your version:
docker exec open-notebook cat /app/package.json | grep version
If version < 1.1.0, you may need:
/api/* routing to port 5055 in reverse proxy/config endpoint routing for versions ≤ 1.0.10:5055 to URL" troubleshooting section aboveRecommendation: Upgrade to v1.1+ for simplified configuration and better performance.