# Edge reverse proxy — single entry point (replaces Caddy).
# Optional upstreams use Docker DNS resolver so nginx starts even if a module is disabled.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Reflect browser Origin for cross-origin API calls (web app on :3004, API on :80).
map $http_origin $cors_allow_origin {
default $http_origin;
'' '*';
}
server {
listen 80;
server_name ${DOMAIN};
client_max_body_size 10G;
# --- ultid /api/v1/* (before OpenWebUI catch-alls; ^~ beats prefix /api/) ---
location ^~ /api/v1/mail/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/migration/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/admin/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/drive/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/office/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/richtext/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/ultidraw/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/ai/mcp {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Mcp-Session-Id, Origin, X-Requested-With, X-Trace-Id, X-Ulti-Token, X-OpenWebUI-User-Email, X-OpenWebUI-User-Id, X-OpenWebUI-User-Name, X-OpenWebUI-User-Role" always;
add_header Access-Control-Expose-Headers "Mcp-Session-Id, X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Ulti-Token $http_x_ulti_token;
proxy_set_header X-OpenWebUI-User-Email $http_x_openwebui_user_email;
proxy_set_header X-OpenWebUI-User-Id $http_x_openwebui_user_id;
proxy_set_header X-OpenWebUI-User-Name $http_x_openwebui_user_name;
proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
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;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location ^~ /api/v1/ai/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Mcp-Session-Id, Origin, X-Requested-With, X-Trace-Id, X-Ulti-Token, X-OpenWebUI-User-Email, X-OpenWebUI-User-Id, X-OpenWebUI-User-Name, X-OpenWebUI-User-Role" always;
add_header Access-Control-Expose-Headers "Mcp-Session-Id, X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Ulti-Token $http_x_ulti_token;
proxy_set_header X-OpenWebUI-User-Email $http_x_openwebui_user_email;
proxy_set_header X-OpenWebUI-User-Id $http_x_openwebui_user_id;
proxy_set_header X-OpenWebUI-User-Name $http_x_openwebui_user_name;
proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
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;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location ^~ /api/v1/users/me {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/calendar/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/contacts/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/meet/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/photos/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
location ^~ /api/v1/search {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
# OpenWebUI trusted-header signin — ultid injects X-Ulti-User-* (auth_request + POST body deadlocks)
location = /api/v1/auths/signin {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream/api/v1/ai/embed-signin;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
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;
}
# --- OpenWebUI API at site root (prod SPA: WEBUI_BASE_URL="" → fetch("/api/…")) ---
# No auth_request here: POST + auth_request deadlocks (embed-auth timeout → 500).
# Ultimail gate is /ai/ HTML; OpenWebUI API auth uses its own JWT (signin via embed-signin).
location ~ ^/api/(config|changelog|version|webhook|usage|models|embeddings|message|chat|tasks)(/|$) {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
proxy_pass http://$openwebui_upstream;
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 $connection_upgrade;
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types application/json text/plain;
sub_filter 'UltiAI (Open WebUI)' 'UltiAI';
sub_filter 'Open WebUI' 'UltiAI';
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location ~ ^/api/v1/(auths|channels|chats|notes|models|knowledge|prompts|tools|skills|memories|folders|groups|files|functions|evaluations|analytics|utils|terminals|automations|calendars|scim|pipelines|tasks|images|audio|retrieval|configs|users|chat|embeddings|messages)(/|$) {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
proxy_pass http://$openwebui_upstream;
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 $connection_upgrade;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# ultid API fallback (must stay after ^~ /api/auth/ — mail OIDC routes)
location /api/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
add_header Access-Control-Expose-Headers "X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
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;
}
# OpenWebUI socket.io (ultid uses /ws without /socket.io)
location ^~ /ws/socket.io {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
proxy_pass http://$openwebui_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# OpenWebUI socket.io under /ai prefix (SvelteKit base /ai — avoid embed-auth on /ai/)
location ^~ /ai/ws/socket.io {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
rewrite ^/ai/ws/socket.io(.*)$ /ws/socket.io$1 break;
proxy_pass http://$openwebui_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location /ws {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_ws_upstream ultid:8080;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Vary Origin always;
proxy_pass http://$ultid_ws_upstream;
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;
}
# OpenWebUI Ollama/OpenAI proxy mounts (SPA uses /ollama, /openai at root)
location ^~ /ollama/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
proxy_pass http://$openwebui_upstream;
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;
}
location ^~ /openai/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
proxy_pass http://$openwebui_upstream;
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;
}
# TipTap / Hocuspocus — proxy WS without redirect (301 breaks upgrade)
location /collab {
resolver 127.0.0.11 valid=10s ipv6=off;
set $hocuspocus_upstream host.docker.internal:1234;
rewrite ^/collab/?(.*)$ /$1 break;
proxy_pass http://$hocuspocus_upstream;
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_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Ultimail OIDC post-login — before Authentik /auth/ (path collision)
location ^~ /auth/complete {
resolver 127.0.0.11 valid=10s ipv6=off;
set $mail_upstream ${MAIL_FRONTEND_UPSTREAM};
proxy_pass http://$mail_upstream;
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 $connection_upgrade;
}
location /auth/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $authentik_upstream authentik-server:9000;
proxy_pass http://$authentik_upstream;
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 $connection_upgrade;
proxy_read_timeout 86400;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
# Permet l’embed du portail Authentik dans la suite (même host + dev Next :3004).
add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:3004 http://127.0.0.1:3004" always;
}
location /meet/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $jitsi_upstream jitsi-web;
rewrite ^/meet/(.*)$ /$1 break;
proxy_pass http://$jitsi_upstream;
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;
}
# Public Nextcloud share links → UltiDrive viewer
location ~ ^/cloud/index\.php/s/([^/]+)/?(.*)$ {
return 301 /drive/s/$1$is_args$args;
}
location /cloud/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $nc_upstream nextcloud;
rewrite ^/cloud/(.*)$ /$1 break;
proxy_pass http://$nc_upstream;
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;
}
location = /cloud {
return 301 /cloud/;
}
# OnlyOffice fetches Nextcloud after StorageUrl host rewrite (URLs lack /cloud prefix).
location ^~ /index.php {
resolver 127.0.0.11 valid=10s ipv6=off;
set $nc_upstream nextcloud;
proxy_pass http://$nc_upstream;
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_read_timeout 300s;
}
# OpenWebUI — same-origin proxy with trusted-header SSO
location = /api/v1/ai/embed-auth {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
proxy_pass http://$ultid_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header Authorization $http_authorization;
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;
}
location /ai/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $openwebui_upstream openwebui:8080;
auth_request /api/v1/ai/embed-auth;
auth_request_set $ulti_user_email $upstream_http_x_ulti_user_email;
auth_request_set $ulti_user_name $upstream_http_x_ulti_user_name;
auth_request_set $ulti_user_role $upstream_http_x_ulti_user_role;
proxy_hide_header X-Frame-Options;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
rewrite ^/ai/?(.*)$ /$1 break;
proxy_pass http://$openwebui_upstream;
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 X-Forwarded-Prefix /ai;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Ulti-User-Email $ulti_user_email;
proxy_set_header X-Ulti-User-Name $ulti_user_name;
proxy_set_header X-Ulti-User-Role $ulti_user_role;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# OpenWebUI serves root-relative /static, /_app, /api — prefix for subpath /ai/
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types text/html text/css application/javascript application/json text/javascript;
sub_filter 'href="/static/' 'href="/ai/static/';
sub_filter 'src="/static/' 'src="/ai/static/';
sub_filter 'href="/_app/' 'href="/ai/_app/';
sub_filter 'src="/_app/' 'src="/ai/_app/';
sub_filter '"/_app/' '"/ai/_app/';
sub_filter "'/_app/" "'/ai/_app/";
sub_filter '"/static/' '"/ai/static/';
sub_filter "'/static/" "'/ai/static/";
sub_filter '"/api/' '"/ai/api/';
sub_filter "'/api/" "'/ai/api/";
sub_filter 'href="/manifest.json' 'href="/ai/manifest.json';
sub_filter '"/manifest.json' '"/ai/manifest.json';
# SvelteKit base path — without this, /ai/ routes 404 (base "" expects site root)
sub_filter 'base: ""' 'base: "/ai"';
sub_filter "base: ''" 'base: "/ai"';
# Keep t="" so socket.io uses default namespace (/), not /ai — see openwebui-subfilters.conf
include /etc/nginx/openwebui-subfilters.conf;
sub_filter 'location.href="/auth"' 'location.href="/ai/auth"';
sub_filter "location.href='/auth'" "location.href='/ai/auth'";
sub_filter '"/auth?"' '"/ai/auth?"';
sub_filter '"/auth"' '"/ai/auth"';
sub_filter '