# 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 :3000, 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; } # 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; } 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:3000) # Prod: set MAIL_FRONTEND_UPSTREAM=suite-frontend:3000 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 ^~ /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; } # 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; } location = / { return 302 /mail/inbox; } location / { default_type text/plain; return 404 "Not found\n"; } }