deer-flow/docker/nginx/nginx.local.conf
Hyeonsang Cho a253faefdf
fix(docker): give project-document uploads their own nginx location so they are not capped at 1 MB (#5934)
* fix(docker): give project-document uploads their own nginx location

POST /api/projects/{id}/documents is a multipart upload that Gateway
accepts up to uploads.max_file_size (50 MiB by default), but no nginx
location matched it. It fell through to the /api/ catch-all, which has no
client_max_body_size, so nginx's 1m default answered a 2 MB PDF with a
bare 413 before Gateway saw the request. Reproduced on nginx:alpine with
the shipped config; the same body to /api/threads/{id}/uploads passed.

Add a `~ ^/api/projects/[^/]+/documents` location to the Docker, make dev
and Helm configs with the thread-uploads settings (100M, streamed body)
and the 600s read timeout the catch-all already granted these routes. The
catch-all keeps nginx's defaults. The shared location-block helpers move
to tests/support/nginx_conf.py so the new test and the existing body-size
test use one implementation.

* docs(changelog): reference #5934 in the project-documents nginx entries
2026-09-27 10:58:54 +08:00

392 lines
15 KiB
Plaintext

# Global error_log (main context). Without this, nginx opens its compiled-in
# default (/var/log/nginx/error.log) at startup before reaching the http-block
# error_log, which fails with permission denied when run as a non-root user.
error_log logs/nginx-error.log warn;
events {
worker_connections 1024;
}
pid logs/nginx.pid;
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Compress textual delivery only. SSE and already-compressed media are
# deliberately absent because buffering/compression hurts their latency.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml;
# Logging
access_log logs/nginx-access.log;
error_log logs/nginx-error.log;
# Upstream servers (using 127.0.0.1 for local development)
upstream gateway {
server 127.0.0.1:8001;
}
upstream frontend {
server 127.0.0.1:3000;
}
# Only mark frontend requests as connection upgrades when the browser
# actually requested one. Next.js dev HMR also uses long-lived HTTP
# streams, and forcing "Connection: upgrade" on those requests makes
# nginx treat the upstream response as an invalid upgrade handshake.
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
server {
listen 2026;
listen [::]:2026;
server_name _;
# Keep the unified nginx endpoint same-origin by default. When split
# frontend/backend or port-forwarded deployments need browser CORS,
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
# CSRF origin checks stay aligned instead of approving every origin at
# the proxy layer.
# LangGraph-compatible API routes served by Gateway.
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
location /api/langgraph/ {
rewrite ^/api/langgraph/(.*) /api/$1 break;
proxy_pass http://gateway;
proxy_http_version 1.1;
# Headers
proxy_set_header Host $http_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 Connection '';
# SSE/Streaming support
proxy_buffering off;
proxy_cache off;
proxy_set_header X-Accel-Buffering no;
# Long chat/text-prompt support (issue #3952): a sufficiently long
# pasted prompt otherwise exceeds nginx's default 1m
# client_max_body_size, or gets spooled to a temp file via
# proxy_request_buffering before reaching Gateway -- on a non-root
# local run that temp directory may not be writable, producing a
# raw nginx 500 instead of a graceful application error. Mirrors
# the uploads location's fix below (same mechanism, sized for text
# prompts rather than binary file uploads).
client_max_body_size 20M;
proxy_request_buffering off;
# Timeouts for long-running requests
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
# Chunked transfer encoding
chunked_transfer_encoding on;
}
# Custom API: Models endpoint
location /api/models {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Disable buffering to avoid permission errors when nginx
# runs as a non-root user (e.g. local development).
proxy_buffering off;
proxy_cache off;
}
# Custom API: Memory endpoint
location /api/memory {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# Custom API: MCP configuration endpoint
location /api/mcp {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# Admin-only local .skill archive upload from Settings.
location = /api/skills/install/upload {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
client_max_body_size 101M;
proxy_request_buffering off;
proxy_read_timeout 600s;
proxy_buffering off;
proxy_cache off;
}
# Custom API: Skills configuration endpoint
location /api/skills {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
# Installing a .skill archive runs one LLM security scan per file, and a
# custom-skill edit or rollback runs one more; none sets its own timeout.
# The upload endpoint above already allows 600s for the same work.
proxy_read_timeout 600s;
}
# Custom API: Agents endpoint
location /api/agents {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# Custom API: Uploads endpoint
location ~ ^/api/threads/[^/]+/uploads {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Large file upload support
client_max_body_size 100M;
proxy_request_buffering off;
# Disable response buffering to avoid permission errors
proxy_buffering off;
proxy_cache off;
}
# Custom API: Project documents (multipart upload, promote-from-thread,
# attach-to-thread, content download, delete). Without its own location
# the upload fell through to /api/ and nginx's 1m default rejected
# anything larger with a 413 before Gateway saw it, although Gateway
# allows uploads.max_file_size (50 MiB by default) on this route. Same
# ceiling and streaming settings as the thread uploads route above; the
# read timeout keeps what /api/ already granted these routes.
location ~ ^/api/projects/[^/]+/documents {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
client_max_body_size 100M;
proxy_request_buffering off;
proxy_read_timeout 600s;
# Disable response buffering to avoid permission errors
proxy_buffering off;
proxy_cache off;
}
# Live browser stream is a WebSocket upgrade. It must be matched before
# the generic /api/threads regex below (which omits Upgrade/Connection
# forwarding and would downgrade it to plain HTTP).
location ~ ^/api/threads/[^/]+/browser/stream {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Long-lived Live stream: keep the upgraded connection open.
proxy_buffering off;
proxy_cache off;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
# Custom API: Other endpoints under /api/threads
location ~ ^/api/threads {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
# /compact and /suggestions hold the response open for a model call,
# /runs/wait for a whole run. nginx's 60s default would 504 them mid-work:
# the compaction still commits, and the waited run is cancelled.
proxy_read_timeout 600s;
}
# API Documentation: Swagger UI
location /api/docs {
proxy_pass http://gateway/docs ;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# API Documentation: ReDoc
location /api/redoc {
proxy_pass http://gateway/redoc;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# API Documentation: OpenAPI Schema
location /openapi.json {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# Health check endpoint (gateway)
location /health {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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_buffering off;
proxy_cache off;
}
# Catch-all for any /api/* prefix not matched by a more specific block above.
# Covers the auth module (/api/v1/auth/login, /me, /change-password, ...),
# plus feedback / runs / token-usage routes that 2.0-rc added without
# updating this nginx config. Longest-prefix matching ensures the explicit
# blocks above (/api/models, /api/threads regex, /api/langgraph/, ...) still
# win for their paths — only truly unmatched /api/* requests land here.
location /api/ {
proxy_pass http://gateway;
proxy_http_version 1.1;
proxy_set_header Host $http_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;
# Auth endpoints set HttpOnly cookies — make sure nginx doesn't
# strip the Set-Cookie header from upstream responses.
proxy_pass_header Set-Cookie;
# Disable buffering to avoid permission errors when nginx
# runs as a non-root user (e.g. local development).
proxy_buffering off;
proxy_cache off;
# /api/runs/wait blocks on a whole run and /api/input-polish on a model
# call. nginx's 60s default would 504 them mid-work, and the waited run is
# cancelled on the disconnect.
proxy_read_timeout 600s;
}
# All other requests go to frontend
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
# Headers
proxy_set_header Host $http_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 $connection_upgrade;
proxy_cache_bypass $http_upgrade;
# Disable response buffering for the frontend. Without this,
# nginx tries to spool large upstream responses (e.g. Next.js
# static chunks) into ``proxy_temp_path``, which defaults to
# the system-owned ``/var/lib/nginx/proxy`` and fails with
# ``[crit] open() ... failed (13: Permission denied)`` when
# nginx is launched as a non-root user (every dev machine
# except production root containers). The symptom on the
# client side is ``ERR_INCOMPLETE_CHUNKED_ENCODING`` and
# ``ChunkLoadError`` partway through page hydration.
#
# Streaming the response straight through avoids the
# temp-file path entirely. The frontend already sets its
# own cache headers, so we don't lose anything from
# disabling nginx-side buffering.
proxy_buffering off;
proxy_request_buffering off;
# Timeouts
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
}
}