diff --git a/docker/.env.example b/docker/.env.example index 5fcff319982..3e1e2dc35e6 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -217,3 +217,22 @@ KONG_HTTPS_PORT=8443 # Enable webp support IMGPROXY_ENABLE_WEBP_DETECTION=true + + +############ +# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt +############ + +# Documentation: +# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https + +# Usage: +# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d +# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d + +# Domain name for the proxy (must point to your server) +PROXY_DOMAIN=your-domain.example.com + +# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN). +# This should be a valid email, not a placehoder (otherwise Certbot may fail to start). +CERTBOT_EMAIL=admin@example.com diff --git a/docker/docker-compose.caddy.yml b/docker/docker-compose.caddy.yml new file mode 100644 index 00000000000..b31ee8db586 --- /dev/null +++ b/docker/docker-compose.caddy.yml @@ -0,0 +1,34 @@ +services: + + kong: + ports: !reset [] + + caddy: + container_name: supabase-caddy + image: caddy:2 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/sh + - -c + - | + PROXY_AUTH_PASSWORD=$$(caddy hash-password --plaintext "$$PROXY_AUTH_PASSWORD") && \ + caddy run --config /etc/caddy/Caddyfile --adapter caddyfile + volumes: + - ./volumes/proxy/caddy:/etc/caddy + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: diff --git a/docker/docker-compose.nginx.yml b/docker/docker-compose.nginx.yml new file mode 100644 index 00000000000..e693a86aee1 --- /dev/null +++ b/docker/docker-compose.nginx.yml @@ -0,0 +1,33 @@ +services: + + kong: + ports: !reset [] + + nginx: + container_name: supabase-nginx + image: jonasal/nginx-certbot:6.0.1-nginx1.29.5 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/bash + - -c + - | + printf '%s:%s\n' "$${PROXY_AUTH_USERNAME}" "$$(openssl passwd -apr1 "$${PROXY_AUTH_PASSWORD}")" > /etc/nginx/user_conf.d/dashboard-passwd && \ + envsubst '$${PROXY_DOMAIN}' < /etc/nginx/supabase-nginx.conf.tpl > /etc/nginx/user_conf.d/nginx.conf && \ + /scripts/start_nginx_certbot.sh + volumes: + - ./volumes/proxy/nginx/supabase-nginx.conf.tpl:/etc/nginx/supabase-nginx.conf.tpl:ro + - nginx_letsencrypt:/etc/letsencrypt + +volumes: + nginx_letsencrypt: diff --git a/docker/volumes/proxy/caddy/Caddyfile b/docker/volumes/proxy/caddy/Caddyfile new file mode 100644 index 00000000000..d76c3ed36f3 --- /dev/null +++ b/docker/volumes/proxy/caddy/Caddyfile @@ -0,0 +1,36 @@ +{$PROXY_DOMAIN} { + @supabase_api path /auth/v1/* /rest/v1/* /graphql/v1/* /realtime/v1/* /functions/v1/* /mcp + + handle @supabase_api { + reverse_proxy kong:8000 + } + + handle_path /storage/v1/* { + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + @cors_preflight method OPTIONS + handle @cors_preflight { + header Access-Control-Allow-Origin * + header Access-Control-Allow-Methods "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS" + header Access-Control-Allow-Headers * + respond 204 + } + + header Access-Control-Allow-Origin * + + reverse_proxy storage:5000 { + # Required for TUS resumable upload Location headers and S3 signature verification. + header_up X-Forwarded-Prefix /{http.request.orig_uri.path.0}/{http.request.orig_uri.path.1} + } + } + + handle { + basic_auth { + # PROXY_AUTH_PASSWORD is overwritten with the bcrypt on startup + {$PROXY_AUTH_USERNAME} {$PROXY_AUTH_PASSWORD} + } + + reverse_proxy studio:3000 + } + + header -server +} diff --git a/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl new file mode 100644 index 00000000000..f50f23653c0 --- /dev/null +++ b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl @@ -0,0 +1,109 @@ +upstream kong_upstream { + server kong:8000; + keepalive 2; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name ${PROXY_DOMAIN}; + server_tokens off; + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + ssl_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${PROXY_DOMAIN}/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/chain.pem; + + ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Prevent 502 errors from large Supabase auth cookies + large_client_header_buffers 4 16k; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + location / { + auth_basic "supabase"; + auth_basic_user_file /etc/nginx/user_conf.d/dashboard-passwd; + + proxy_pass http://studio:3000; + } + + location /auth { + proxy_pass http://kong_upstream; + } + + location /rest { + proxy_pass http://kong_upstream; + } + + location /realtime/v1/ { + proxy_pass http://kong_upstream; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 3600s; + } + + location /storage/v1/ { + proxy_pass http://storage:5000/; + proxy_buffering off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + # Required for TUS resumable upload Location headers and S3 signature verification. + proxy_set_header X-Forwarded-Prefix /storage/v1; + + client_max_body_size 0; + + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' '*'; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + } + + location /functions { + proxy_pass http://kong_upstream; + } + + location /graphql { + proxy_pass http://kong_upstream; + } + + location /mcp { + proxy_pass http://kong_upstream; + } +}