# 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 (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; } 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; } # 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/ { proxy_pass http://authentik-server:9000; 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; } 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 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; } location = /ai { return 301 /ai/; } 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) — 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; } # 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; } 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; } location / { default_type text/plain; return 404 "Not found\n"; } }