# 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/ { 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/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_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"'; # In-app links that escape to site root (e.g. "Nouvelle conversation" → /) sub_filter 'href="/"' 'href="/ai/"'; sub_filter "href='/'" "href='/ai/'"; sub_filter 'href="/notes' 'href="/ai/notes'; sub_filter 'href="/workspace' 'href="/ai/workspace'; sub_filter 'href="/home' 'href="/ai/home'; sub_filter 'href="/c/' 'href="/ai/c/'; sub_filter 'href="/automations' 'href="/ai/automations'; sub_filter 'href="/calendar' 'href="/ai/calendar'; sub_filter 'href="/channels' 'href="/ai/channels'; sub_filter 'href="/playground' 'href="/ai/playground'; # UltiAI branding (WEBUI_NAME env still appends " (Open WebUI)" in backend) sub_filter 'UltiAI (Open WebUI)' 'UltiAI'; sub_filter 'Open WebUI' 'UltiAI'; } location = /ai { return 301 /ai/; } # OpenWebUI API (prefix /ai/api — évite collision avec ultid /api/v1/) location /ai/api/ { resolver 127.0.0.11 valid=10s ipv6=off; set $openwebui_upstream openwebui:8080; rewrite ^/ai/api/?(.*)$ /api/$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 Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # OpenWebUI assets (SPA uses root-relative /static, /_app — not /ai/…) location ^~ /static/ { 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_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-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_set_header Accept-Encoding ""; sub_filter_once off; sub_filter_types application/javascript text/javascript; sub_filter '"/api/' '"/ai/api/'; sub_filter "'/api/" "'/ai/api/"; sub_filter 'href="/"' 'href="/ai/"'; sub_filter "href='/'" "href='/ai/'"; sub_filter 'Open WebUI' 'UltiAI'; sub_filter 'UltiAI (Open WebUI)' 'UltiAI'; } location ^~ /_app/ { 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_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-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_set_header Accept-Encoding ""; sub_filter_once off; sub_filter_types application/javascript text/javascript application/json; sub_filter '"/api/' '"/ai/api/'; sub_filter "'/api/" "'/ai/api/"; sub_filter 'href="/"' 'href="/ai/"'; sub_filter "href='/'" "href='/ai/'"; sub_filter 'Open WebUI' 'UltiAI'; sub_filter 'UltiAI (Open WebUI)' 'UltiAI'; } location = /manifest.json { 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_pass http://$openwebui_upstream/manifest.json; 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-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; } location /office/ { resolver 127.0.0.11 valid=10s ipv6=off; set $oo_upstream onlyoffice; rewrite ^/office/(.*)$ /$1 break; proxy_pass http://$oo_upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host/office; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; } location = /office { return 301 /office/; } # UltiDrive — same suite frontend as mail (unified Next.js app) location /drive/ { 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 = /drive { 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; } # Ulti Suite frontend (mail + drive + contacts + agenda + compte) — dev: pnpm dev on host (MAIL_FRONTEND_UPSTREAM=host.docker.internal:3004) # Prod: set MAIL_FRONTEND_UPSTREAM=suite-frontend:3000 # Démos publiques de la landing (zéro rétention) — frontend Next. location ^~ /demo { 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; } # Sauvegarde no-op des démos — route API du frontend Next, pas ultid. location ^~ /api/demo/ { 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; } location ^~ /api/auth/ { 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; } location ^~ /mail/ { 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; } # Do not 301 /mail → /mail/ (Next 308 /mail/ → /mail causes a redirect loop). location = /mail { 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 = /mail/ { return 302 /mail/inbox; } location ^~ /login { 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 ^~ /chat { 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 ^~ /contacts { 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 ^~ /agenda { 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; } # Réglages du compte Ulti location ^~ /compte { 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; } # Console d'administration suite location ^~ /admin { 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 ^~ /_next/ { 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; } # next/font (Geist, etc.) — separate from /_next/ static chunks location ^~ /__nextjs_font/ { 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; } location ^~ /brand/ { 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-Forwarded-Proto $scheme; } location ^~ /mail-backgrounds/ { 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-Forwarded-Proto $scheme; } # Public assets at repo root (launcher icons, etc.) location ~* ^/[^/]+\.(svg|png|ico|webp)$ { resolver 127.0.0.11 valid=10s ipv6=off; set $mail_upstream ${MAIL_FRONTEND_UPSTREAM}; proxy_pass http://$mail_upstream$request_uri; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } # Landing page de la suite (servie par le frontend Next). location = / { 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; } # OpenWebUI SPA routes without /ai prefix (client nav + hard reload in UltiAI iframe). # Do not include "auth" — Ultimail OIDC uses /auth/application/o/… (Authentik). location ~ ^/(notes|workspace|c|home|automations|calendar|channels|playground|watch|s|error)(/|$) { return 302 /ai$uri$is_args$args; } location / { default_type text/plain; return 404 "Not found\n"; } } # Stalwart webadmin + JMAP (mail.${DOMAIN}) server { listen 80; server_name mail.${DOMAIN}; client_max_body_size 100M; location / { resolver 127.0.0.11 valid=10s ipv6=off; set $stalwart_upstream stalwart:8080; proxy_pass http://$stalwart_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; } }