open-notebook/docs/deployment/reverse-proxy.md
Luis Novo 9bdfd99f1b
Some checks are pending
Development Build / extract-version (push) Waiting to run
Development Build / test-build-regular (push) Blocked by required conditions
Development Build / test-build-single (push) Blocked by required conditions
Development Build / summary (push) Blocked by required conditions
feat: simplify reverse proxy configuration with Next.js rewrites (#213)
* feat: simplify reverse proxy configuration with Next.js rewrites

Add Next.js API rewrites to proxy /api/* requests internally from port 8502
to the FastAPI backend on port 5055. This eliminates the need for complex
reverse proxy configurations with multiple upstreams and location blocks.

Changes:
- Add rewrites to next.config.ts proxying /api/* to INTERNAL_API_URL
- Introduce INTERNAL_API_URL env var (defaults to http://localhost:5055)
- Update supervisord configs to pass INTERNAL_API_URL to Next.js
- Document INTERNAL_API_URL in .env.example with usage examples
- Add simplified reverse proxy examples for nginx, Traefik, Caddy, Coolify
- Update README architecture diagram to show internal proxying
- Add explanatory comments to _config route handler

Benefits:
- Reduces reverse proxy config from 12 lines to 3 (75% reduction)
- Single-port deployment (8502 only) for 95% of use cases
- Zero breaking changes - backward compatible with existing setups
- Zero performance overhead (validated through testing)
- Preserves proxy headers (X-Forwarded-*) for rate limiting/SSL

Resolves: #179
Related: OSS-321

* fix: rename _config to config to fix production routing

CRITICAL BUG FIX: The /_config endpoint has never worked in production builds
because Next.js treats folders starting with underscore as "private folders"
and excludes them from routing entirely.

This endpoint is critical for:
- Providing API_URL to the browser at runtime
- Enabling zero-config deployments with auto-detection
- Supporting reverse proxy scenarios where API URL differs from frontend URL

Changes:
- Rename frontend/src/app/_config/ → frontend/src/app/config/
- Update client code references (/_config → /config)
- Update documentation with correct endpoint path
- Bump version to 1.1.0 (minor version for new rewrites feature + bug fix)

Impact:
- Runtime configuration now works in production builds
- /config returns {"apiUrl":"http://localhost:5055"} correctly
- Auto-detection for reverse proxy deployments now functional

Related: #179, OSS-321

* fix: resolve React hook exhaustive-deps warning in AddExistingSourceDialog

Wrap performSearch function in useCallback to properly memoize it and satisfy
React Hook exhaustive-deps rule. This prevents unnecessary re-renders and
ensures the useEffect dependency array is correctly specified.

Changes:
- Import useCallback from React
- Wrap performSearch with useCallback([debouncedSearchQuery, allSources])
- Add performSearch to useEffect dependency array

* final fixes
2025-10-24 11:24:14 -03:00

14 KiB

Reverse Proxy Configuration

This guide helps you deploy Open Notebook behind a reverse proxy (nginx, Caddy, Traefik, etc.) or with a custom domain.

Simplified Configuration (v1.1+)

Starting with v1.1, Open Notebook uses Next.js rewrites to dramatically simplify reverse proxy configuration. You now only need to proxy to port 8502 - Next.js handles internal API routing automatically.

How It Works

Browser → Reverse Proxy → Port 8502 (Next.js)
                             ↓ (internal proxy)
                          Port 5055 (FastAPI)

Next.js rewrites automatically forward /api/* requests to the FastAPI backend on port 5055, so your reverse proxy only needs to know about one port!

Simple Configuration Examples

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;

    # 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;
    }
}

Traefik

services:
  open-notebook:
    image: lfnovo/open_notebook:v1-latest-single
    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"
    networks:
      - traefik-network

Caddy

notebook.example.com {
    reverse_proxy open-notebook:8502
}

Coolify

  1. Create a new service pointing to lfnovo/open_notebook:v1-latest-single
  2. Set port to 8502 (not 5055!)
  3. Add environment variable: API_URL=https://your-domain.com
  4. Enable HTTPS in Coolify settings
  5. Done! Coolify handles the reverse proxy automatically.

Environment Variables

With the simplified approach, you typically only need:

# Required for reverse proxy setups
API_URL=https://your-domain.com

# Optional: Only needed for multi-container deployments
# Default is http://localhost:5055 (single-container)
# INTERNAL_API_URL=http://api-service:5055

Optional: Direct API Access for External Integrations

If you have external scripts or integrations that need direct API access, you can still route /api/* directly to port 5055:

# Optional: Direct API access (for external integrations only)
location /api/ {
    proxy_pass http://open-notebook:5055/api/;
    # ... same headers as above
}

# Primary route (handles browser traffic)
location / {
    proxy_pass http://open-notebook:8502;
    # ... same headers as above
}

Note: The simplified single-port approach (port 8502 only) works for 95% of use cases. Only add direct API routing if you specifically need it.


Legacy Configuration (Pre-v1.1)

Note

: The configurations below are still supported but no longer necessary with v1.1+. New deployments should use the simplified configuration above.

The API_URL Environment Variable

Starting with v1.0+, Open Notebook supports runtime configuration of the API URL through the API_URL environment variable. This means you can use the same Docker image in different deployment scenarios without rebuilding.

How It Works

The frontend uses a three-tier priority system to determine the API URL:

  1. Runtime Configuration (Highest Priority): API_URL environment variable set at container runtime
  2. Build-time Configuration: NEXT_PUBLIC_API_URL baked into the Docker image
  3. Auto-detection (Fallback): Infers from the incoming HTTP request headers

Auto-detection details:

  • The Next.js frontend analyzes the incoming HTTP request
  • Extracts the hostname from the host header
  • Respects the X-Forwarded-Proto header (for HTTPS behind reverse proxies)
  • Constructs the API URL as {protocol}://{hostname}:5055
  • Example: Request to http://10.20.30.20:8502 → API URL becomes http://10.20.30.20:5055

Common Scenarios

Scenario 1: Docker on Localhost (Default)

No configuration needed! The system auto-detects.

docker run -d \
  --name open-notebook \
  -p 8502:8502 -p 5055:5055 \
  -v ./notebook_data:/app/data \
  -v ./surreal_data:/mydata \
  lfnovo/open_notebook:v1-latest-single

Scenario 2: Docker on Remote Server (LAN/VPS)

Access via IP address - auto-detection works, but you can be explicit:

docker run -d \
  --name open-notebook \
  -p 8502:8502 -p 5055:5055 \
  -e API_URL=http://192.168.1.100:5055 \
  -v ./notebook_data:/app/data \
  -v ./surreal_data:/mydata \
  lfnovo/open_notebook:v1-latest-single

Note

: Don't include /api at the end - the system adds this automatically!

Scenario 3: Behind Reverse Proxy with Custom Domain

This is where API_URL is essential. Your reverse proxy handles HTTPS and routing.

Important: If your reverse proxy forwards /api requests to the backend, set API_URL to just the domain (without /api suffix). The frontend will append /api automatically.

Example: nginx + Docker Compose

docker-compose.yml:

version: '3.8'

services:
  open-notebook:
    image: lfnovo/open_notebook:v1-latest-single
    container_name: open-notebook
    environment:
      - API_URL=https://notebook.example.com
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - ./notebook_data:/app/data
      - ./surreal_data:/mydata
    ports:
      - "8502:8502"  # Frontend
      - "5055:5055"  # API
    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

nginx.conf:

http {
    upstream frontend {
        server open-notebook:8502;
    }

    upstream api {
        server open-notebook:5055;
    }

    server {
        listen 80;
        server_name notebook.example.com;
        return 301 https://$server_name$request_uri;
    }

    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;

        # API
        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 - handles /config automatically)
        location / {
            proxy_pass http://frontend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            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_cache_bypass $http_upgrade;
        }
    }
}

Scenario 4: Behind Reverse Proxy with Subdomain

If you want API on a separate subdomain:

docker-compose.yml:

services:
  open-notebook:
    image: lfnovo/open_notebook:v1-latest-single
    environment:
      - API_URL=https://api.notebook.example.com
      # ... other env vars

nginx.conf:

# Frontend server
server {
    listen 443 ssl http2;
    server_name notebook.example.com;

    location / {
        proxy_pass http://open-notebook:8502;
        # ... proxy headers
    }
}

# API server
server {
    listen 443 ssl http2;
    server_name api.notebook.example.com;

    location / {
        proxy_pass http://open-notebook:5055;
        # ... proxy headers
    }
}

Scenario 5: Traefik

docker-compose.yml:

version: '3.8'

services:
  open-notebook:
    image: lfnovo/open_notebook:v1-latest-single
    environment:
      - API_URL=https://notebook.example.com
    labels:
      # Frontend
      - "traefik.enable=true"
      - "traefik.http.routers.notebook-frontend.rule=Host(`notebook.example.com`)"
      - "traefik.http.routers.notebook-frontend.entrypoints=websecure"
      - "traefik.http.routers.notebook-frontend.tls.certresolver=myresolver"
      - "traefik.http.services.notebook-frontend.loadbalancer.server.port=8502"

      # API (higher priority to match first)
      - "traefik.http.routers.notebook-api.rule=Host(`notebook.example.com`) && PathPrefix(`/api`)"
      - "traefik.http.routers.notebook-api.entrypoints=websecure"
      - "traefik.http.routers.notebook-api.tls.certresolver=myresolver"
      - "traefik.http.routers.notebook-api.priority=100"
      - "traefik.http.services.notebook-api.loadbalancer.server.port=5055"
    networks:
      - traefik-network

networks:
  traefik-network:
    external: true

Scenario 6: Caddy

Caddyfile:

notebook.example.com {
    # API
    reverse_proxy /api/* open-notebook:5055

    # Frontend (catch-all - handles /config automatically)
    reverse_proxy / open-notebook:8502
}

docker-compose.yml:

services:
  open-notebook:
    image: lfnovo/open_notebook:v1-latest-single
    environment:
      - API_URL=https://notebook.example.com
    # No need to expose ports if using Caddy in same network

Troubleshooting

Connection Error: Unable to connect to server

Symptoms: Frontend displays "Unable to connect to server. Please check if the API is running."

Possible Causes:

  1. API_URL not set correctly for your reverse proxy setup

    • Check browser console (F12) for connection errors
    • Look for logs showing what URL the frontend is trying
  2. Reverse proxy not forwarding to correct port

    • API should be accessible at the URL specified in API_URL
    • Test: curl https://your-domain.com/api/config should return JSON
  3. CORS issues

    • Ensure X-Forwarded-Proto and X-Forwarded-For headers are set in proxy config
    • Check API logs for CORS errors
  4. SSL/TLS certificate issues

    • Ensure your reverse proxy has valid SSL certificates
    • Mixed content errors (HTTPS frontend trying to reach HTTP API)

Frontend adds :5055 to URL when using reverse proxy (versions ≤ 1.0.10)

Symptoms (only in versions 1.0.10 and earlier):

Root Cause: In versions ≤ 1.0.10, the frontend's config endpoint was at /api/runtime-config, which gets 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.

Note: Most reverse proxy configurations with a catch-all rule like location / { proxy_pass http://frontend; } will automatically route /config to the frontend without any additional configuration needed.

Only if you have issues, explicitly configure the /config route:

# Only needed if your reverse proxy doesn't have a catch-all rule
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;
}

Verification: Check browser console (F12) - should see: ✅ [Config] Runtime API URL from server: https://your-domain.com

How to Debug

  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
  2. Test API directly:

    # Should return JSON config
    curl https://your-domain.com/api/config
    
  3. Check Docker logs:

    docker logs open-notebook
    
    • Look for frontend and API startup messages
    • Check for connection errors
  4. Verify environment variable:

    docker exec open-notebook env | grep API_URL
    

Missing Authorization Header

Symptoms: API returns {"detail": "Missing authorization header"}

This happens when:

  • You have set OPEN_NOTEBOOK_PASSWORD for authentication
  • You're trying to access /api/config directly without logging in first

Solution: This is expected behavior! The frontend handles this automatically. Just access the frontend URL and log in through the UI.

Best Practices

  1. Always use HTTPS in production with reverse proxies
  2. Set API_URL explicitly when using reverse proxies to avoid auto-detection issues
  3. Use environment files (.env or docker.env) to manage configuration
  4. Test your setup by accessing the frontend and checking browser console logs
  5. Keep ports 5055 and 8502 accessible from your reverse proxy container

Additional Resources