Implement a serverside fix for reverse proxy users (#169)

This commit is contained in:
Luis Novo 2025-10-19 08:02:21 -03:00 committed by GitHub
parent 2fa2956c4c
commit 04b5a9c96a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 409 additions and 20 deletions

View file

@ -1,8 +1,22 @@
# API CONFIGURATION
# URL where the API can be accessed by the browser
# Default: http://localhost:5055 (works for most Docker setups)
# Change this if you're running on a different host/port (e.g., http://your-server-ip:5055)
# This setting allows the frontend to connect to the API at runtime (no rebuild needed!)
#
# IMPORTANT: Do NOT include /api at the end - it will be added automatically!
#
# Common scenarios:
# - Docker on localhost: http://localhost:5055 (default, works for most cases)
# - Docker on LAN/remote server: http://192.168.1.100:5055 or http://your-server-ip:5055
# - Behind reverse proxy with custom domain: https://your-domain.com
# - Behind reverse proxy with subdomain: https://api.your-domain.com
#
# Examples for reverse proxy users:
# - API_URL=https://notebook.example.com (frontend will call https://notebook.example.com/api/*)
# - API_URL=https://api.example.com (frontend will call https://api.example.com/api/*)
#
# Note: If not set, the system will auto-detect based on the incoming request.
# Only set this if you need to override the auto-detection (e.g., reverse proxy scenarios).
API_URL=http://localhost:5055
# SECURITY

View file

@ -84,7 +84,14 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Create log directories
RUN mkdir -p /var/log/supervisor
# No default API_URL - the API will auto-detect from incoming requests
# Users can still override by setting API_URL environment variable if needed
# Runtime API URL Configuration
# The API_URL environment variable can be set at container runtime to configure
# where the frontend should connect to the API. This allows the same Docker image
# to work in different deployment scenarios without rebuilding.
#
# If not set, the system will auto-detect based on incoming requests.
# Set API_URL when using reverse proxies or custom domains.
#
# Example: docker run -e API_URL=https://your-domain.com/api ...
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -88,7 +88,14 @@ COPY supervisord.single.conf /etc/supervisor/conf.d/supervisord.conf
# Create log directories
RUN mkdir -p /var/log/supervisor
# No default API_URL - the API will auto-detect from incoming requests
# Users can still override by setting API_URL environment variable if needed
# Runtime API URL Configuration
# The API_URL environment variable can be set at container runtime to configure
# where the frontend should connect to the API. This allows the same Docker image
# to work in different deployment scenarios without rebuilding.
#
# If not set, the system will auto-detect based on incoming requests.
# Set API_URL when using reverse proxies or custom domains.
#
# Example: docker run -e API_URL=https://your-domain.com/api ...
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -29,7 +29,14 @@ This section provides comprehensive guides for deploying Open Notebook in differ
- Development tools and debugging
- Contributing to the project
### 4. [Security Configuration](security.md)
### 4. [Reverse Proxy Configuration](reverse-proxy.md)
**For production deployments with custom domains**
- nginx, Caddy, Traefik configurations
- Custom domain setup
- SSL/HTTPS configuration
- Runtime API URL configuration
### 5. [Security Configuration](security.md)
**Essential for public deployments**
- Password protection setup
- Security best practices
@ -50,6 +57,12 @@ This section provides comprehensive guides for deploying Open Notebook in differ
- You have resource constraints
- You don't need to scale services independently
### Use Reverse Proxy Setup if:
- You're deploying with a custom domain
- You need HTTPS/SSL encryption
- You're using nginx, Caddy, or Traefik
- You want to expose only specific ports publicly
### Use Development Setup if:
- You want to contribute to the project
- You need to modify the source code

View file

@ -0,0 +1,304 @@
# Reverse Proxy Configuration
This guide helps you deploy Open Notebook behind a reverse proxy (nginx, Caddy, Traefik, etc.) or with a custom domain.
## 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
## Common Scenarios
### Scenario 1: Docker on Localhost (Default)
No configuration needed! The system auto-detects.
```bash
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:
```bash
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:**
```yaml
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:**
```nginx
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;
# Frontend
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;
}
# 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;
}
}
}
```
### Scenario 4: Behind Reverse Proxy with Subdomain
If you want API on a separate subdomain:
**docker-compose.yml:**
```yaml
services:
open-notebook:
image: lfnovo/open_notebook:v1-latest-single
environment:
- API_URL=https://api.notebook.example.com
# ... other env vars
```
**nginx.conf:**
```nginx
# 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:**
```yaml
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
- "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.services.notebook-api.loadbalancer.server.port=5055"
networks:
- traefik-network
networks:
traefik-network:
external: true
```
### Scenario 6: Caddy
**Caddyfile:**
```caddy
notebook.example.com {
# Frontend
reverse_proxy / open-notebook:8502
# API
reverse_proxy /api/* open-notebook:5055
}
```
**docker-compose.yml:**
```yaml
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)
### 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**:
```bash
# Should return JSON config
curl https://your-domain.com/api/config
```
3. **Check Docker logs**:
```bash
docker logs open-notebook
```
- Look for frontend and API startup messages
- Check for connection errors
4. **Verify environment variable**:
```bash
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
- [Docker Deployment Guide](./docker.md)
- [Security Guide](./security.md)
- [Troubleshooting](../troubleshooting/common-issues.md)

View file

@ -0,0 +1,22 @@
import { NextResponse } from 'next/server'
/**
* Runtime Configuration Endpoint
*
* This endpoint provides server-side environment variables to the client at runtime.
* This solves the NEXT_PUBLIC_* limitation where variables are baked into the build.
*
* Users can now set API_URL in their docker.env and it will be picked up at runtime,
* allowing the same Docker image to work in different deployment scenarios.
*/
export async function GET() {
// Priority:
// 1. API_URL from environment (set by user at runtime)
// 2. NEXT_PUBLIC_API_URL from build time (fallback)
// 3. Default to localhost:5055
const apiUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5055'
return NextResponse.json({
apiUrl,
})
}

View file

@ -60,11 +60,30 @@ async function fetchConfig(): Promise<AppConfig> {
console.log('🔧 [Config] Starting configuration detection...')
console.log('🔧 [Config] Build time:', BUILD_TIME)
// Try to get from environment variable first (for development)
// STEP 1: Try to get runtime config from Next.js server-side API route
// This allows API_URL to be set at runtime (not baked into build)
let runtimeApiUrl: string | null = null
try {
console.log('🔧 [Config] Attempting to fetch runtime config from Next.js API route...')
const runtimeResponse = await fetch('/api/runtime-config', {
cache: 'no-store',
})
if (runtimeResponse.ok) {
const runtimeData = await runtimeResponse.json()
runtimeApiUrl = runtimeData.apiUrl
console.log('✅ [Config] Runtime API URL from server:', runtimeApiUrl)
} else {
console.log('⚠️ [Config] Runtime config endpoint returned status:', runtimeResponse.status)
}
} catch (error) {
console.log('⚠️ [Config] Could not fetch runtime config:', error)
}
// STEP 2: Fallback to build-time environment variable
const envApiUrl = process.env.NEXT_PUBLIC_API_URL
console.log('🔧 [Config] NEXT_PUBLIC_API_URL from build:', envApiUrl || '(not set)')
// Smart default: infer API URL from current frontend URL
// STEP 3: Smart default - infer API URL from current frontend URL
// If frontend is at http://10.20.30.20:8502, API should be at http://10.20.30.20:5055
let defaultApiUrl = 'http://localhost:5055'
@ -82,13 +101,16 @@ async function fetchConfig(): Promise<AppConfig> {
}
}
// Use env var if available, otherwise smart default
const baseUrl = envApiUrl || defaultApiUrl
// Priority: Runtime config > Build-time env var > Smart default
const baseUrl = runtimeApiUrl || envApiUrl || defaultApiUrl
console.log('🔧 [Config] Final base URL to try:', baseUrl)
console.log('🔧 [Config] Selection priority: runtime=' + (runtimeApiUrl ? '✅' : '❌') +
', build-time=' + (envApiUrl ? '✅' : '❌') +
', smart-default=' + (!runtimeApiUrl && !envApiUrl ? '✅' : '❌'))
try {
console.log('🔧 [Config] Fetching runtime config from:', `${baseUrl}/api/config`)
// Try to fetch runtime config from API
console.log('🔧 [Config] Fetching backend config from:', `${baseUrl}/api/config`)
// Try to fetch runtime config from backend API
const response = await fetch(`${baseUrl}/api/config`, {
cache: 'no-store',
})

View file

@ -1,6 +1,6 @@
[project]
name = "open-notebook"
version = "1.0.1"
version = "1.0.2"
description = "An open source implementation of a research assistant, inspired by Google Notebook LM"
authors = [
{name = "Luis Novo", email = "lfnovo@gmail.com"}

View file

@ -28,7 +28,7 @@ startsecs=3
[program:frontend]
command=npm run start
directory=/app/frontend
environment=NODE_ENV="production",PORT="8502"
environment=NODE_ENV="production",PORT="8502",API_URL="%(ENV_API_URL)s"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr

View file

@ -40,7 +40,7 @@ startsecs=3
[program:frontend]
command=npm run start
directory=/app/frontend
environment=NODE_ENV="production",PORT="8502"
environment=NODE_ENV="production",PORT="8502",API_URL="%(ENV_API_URL)s"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr

View file

@ -620,15 +620,15 @@ wheels = [
[[package]]
name = "esperanto"
version = "2.7.0"
version = "2.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/cf/0da02a603a63b3850abd14d23629f101942db5c18840b0cc6f34d7db9a04/esperanto-2.7.0.tar.gz", hash = "sha256:3861e4e20697813b19f0070a1142934bd6792077c3c174a2c3dd4b6ca0676b06", size = 553433, upload-time = "2025-10-19T02:04:30.21Z" }
sdist = { url = "https://files.pythonhosted.org/packages/54/8c/a2a9c1e7428d700963e7450aba591b2f4fbcfa61bad88654af8d92df3a00/esperanto-2.7.1.tar.gz", hash = "sha256:a18abc38d30d38c496eac62f252a9c12d243fc49fef7cc92d9d23c5b5faee741", size = 554840, upload-time = "2025-10-19T02:30:45.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/9c/79827f246965ed66ae8d2f3e3937e552730eaf48b270dac852a4756c7bf4/esperanto-2.7.0-py3-none-any.whl", hash = "sha256:2ea3fa98d8622d08a18dc6701ad362461de02492a3252326c70c969b3aba3db6", size = 129524, upload-time = "2025-10-19T02:04:28.57Z" },
{ url = "https://files.pythonhosted.org/packages/ee/91/a2c43a4eb3c56f349d59c656783cdd8cd13149a8e521e75f77e84dcd5ac0/esperanto-2.7.1-py3-none-any.whl", hash = "sha256:ea81e531085e32828b2ecd02680088801222dd21e7a73012efdb9b68fa627ed4", size = 129593, upload-time = "2025-10-19T02:30:44.149Z" },
]
[[package]]
@ -2199,7 +2199,7 @@ wheels = [
[[package]]
name = "open-notebook"
version = "1.0.1"
version = "1.0.2"
source = { editable = "." }
dependencies = [
{ name = "ai-prompter" },