Part 3 of 7 — Self-hosting Supabase: a learning journey
Also available in French: Partie 3 — Traefik et HTTPS
We need a reverse proxy. It sits in front of all our containers, terminates TLS, and routes incoming requests to the right service based on the hostname. Traefik is the standard choice for Docker-based setups because it can read container labels and configure itself automatically.
Add a label to a container saying "I want a route at this domain" and Traefik creates it. This is elegant. It also has a compatibility problem I ran into immediately, and that costed me quite some time.
The version problem
Recent versions of Docker Engine changed the way the API negotiates versions with clients. Traefik v2, the version you will find in most tutorials, does not handle this correctly. It connects to the Docker daemon, appears to start fine, but silently fails to pick up any services.
Your containers are running. Traefik is running. Nothing gets routed. There is no error message that points clearly at the cause.
The fix is Traefik v3, the current major release. Traefik v3 handles the version negotiation correctly.
If you search for "Traefik Supabase Docker," the majority of results still show Traefik v2 config. Be aware of this before you copy anything.
DNS setup
Before configuring Traefik, you need DNS records pointing to your server. In your DNS provider, create A records for each subdomain you plan to use:
kong.project1.yourdomain.com A YOUR_VPS_IP
studio.project1.yourdomain.com A YOUR_VPS_IP
kong.project2.yourdomain.com A YOUR_VPS_IP
studio.project2.yourdomain.com A YOUR_VPS_IP
Let's Encrypt verifies these records when issuing certificates. DNS propagation typically takes a few minutes, but can take up to a few hours depending on your provider. Before deploying, verify that propagation is complete. If dig is available (apt install dnsutils -y), run dig kong.project1.yourdomain.com +short and confirm it returns your VPS IP. If not, wait and try again.
The Traefik stack
Create traefik/docker-compose.yml:
networks:
traefik_default:
driver: overlay
attachable: true
services:
traefik:
image: traefik:v3
command:
- "--providers.swarm.network=traefik_default"
- "--providers.swarm.exposedByDefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=your@email.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
- "--log.level=INFO"
- "--api.dashboard=false"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
networks:
- traefik_default
deploy:
placement:
constraints:
- node.role == manager
volumes:
letsencrypt:
A few things to explain.
exposedByDefault=false means Traefik ignores any container that does not explicitly opt in with traefik.enable=true. Without this, every container on the Traefik network would be publicly accessible. That is not what you want.
api.dashboard=false disables the Traefik web interface. The dashboard shows your full routing configuration: all service names, all domains, all middleware. There is no reason to expose this publicly.
The letsencrypt volume stores the certificates. Do not delete it between deployments. Let's Encrypt rate-limits certificate requests per domain per week. If you keep clearing the volume and redeploying, you will eventually hit the limit and be unable to get a new certificate for several days.
HTTP traffic on port 80 is redirected to HTTPS automatically by the entrypoint redirect rules.
A label placement problem
In regular Docker Compose, service labels go at the service level:
services:
myapp:
labels:
traefik.enable: 'true'
In Docker Swarm with docker stack deploy, they must go inside the deploy block:
services:
myapp:
deploy:
labels:
traefik.enable: 'true'
Labels at the wrong level are silently ignored. Traefik will not route the service and will give no indication why. I discovered this by staring at a correct-looking configuration for a long time before I found it.
Security headers
Traefik supports middleware: reusable components that process requests before they reach a service. We define a security-headers middleware that adds HSTS, removes the Server header, sets a content type policy, and so on.
In Swarm, middleware defined in one stack is available to other stacks using the @swarm suffix. We define the headers middleware in the project1 stack labels (shown in the next post), and project2 references it as security-headers@swarm. Traefik picks it up automatically when project1 is deployed.
The headers middleware looks like this in the service labels:
traefik.http.middlewares.security-headers.headers.stsSeconds: '63072000'
traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains: 'true'
traefik.http.middlewares.security-headers.headers.stsPreload: 'true'
traefik.http.middlewares.security-headers.headers.forceSTSHeader: 'true'
traefik.http.middlewares.security-headers.headers.contentTypeNosniff: 'true'
traefik.http.middlewares.security-headers.headers.referrerPolicy: strict-origin-when-cross-origin
traefik.http.middlewares.security-headers.headers.customFrameOptionsValue: SAMEORIGIN
traefik.http.middlewares.security-headers.headers.customResponseHeaders.Server: ''
That last line sets the Server response header to an empty string. By default, Kong advertises its version in every response. There is no good reason to do that.
One note on the cross-stack dependency: since the security-headers middleware is defined in project1's stack, it disappears if you tear project1 down entirely. Project2 would lose its headers middleware. For a learning setup this is acceptable. A cleaner solution would be to define shared middleware in the Traefik stack itself.
How Kong gets its route
Only two services in our Supabase stack need a public route: Kong (the API gateway) and Studio (the dashboard). Everything else is internal.
Kong's service labels in our Compose file look like this:
services:
kong:
networks:
- internal
- traefik_default
deploy:
labels:
traefik.enable: 'true'
traefik.http.routers.p1-kong.entrypoints: websecure
traefik.http.routers.p1-kong.rule: Host(`kong.project1.yourdomain.com`)
traefik.http.routers.p1-kong.tls.certresolver: le
traefik.http.routers.p1-kong.middlewares: security-headers@swarm
traefik.http.services.p1-kong.loadbalancer.server.port: '8000'
traefik.swarm.network: traefik_default
Three things matter here.
The router name is p1-kong. Every router name must be unique across all stacks running in Swarm. If two services both register a router named kong, Traefik picks one and ignores the other with no warning. We prefix with the project ID. This becomes important in Post 6 when we add the second instance.
The traefik.swarm.network label tells Traefik which network to use when forwarding requests. Kong is connected to two networks: the internal Supabase network and the Traefik network. Without this label, Traefik might try to forward through the internal network, which it cannot reach.
The loadbalancer.server.port tells Traefik which port to forward to inside the container. We do not publish Kong's port to the host, so Traefik needs to know the container port directly.
Deploy Traefik
docker stack deploy -c traefik/docker-compose.yml traefik
Check that it started:
docker service ls
# NAME MODE REPLICAS
# traefik_traefik replicated 1/1
Verify SSL later
After we deploy a Supabase project in the next post, verify that the certificate was issued:
curl -I https://kong.project1.yourdomain.com/health
# HTTP/2 200
# strict-transport-security: max-age=63072000; includeSubDomains; preload
And that the Server header is gone from the response.
Alternatives to Traefik
This series uses Traefik because it integrates naturally with Docker Swarm via container labels. Supabase now officially documents Caddy and nginx as simpler alternatives in their self-hosting guides — worth reading if you prefer a less label-heavy setup.
One gap in the approach described here: the official Supabase proxy guide recommends routing Storage traffic directly to the storage container, bypassing Kong. Kong adds unnecessary overhead for large file uploads and downloads. If Storage is important to your use case, check the official guide for the direct-proxy configuration.
Part 4 — The first Supabase instance →
The full series
- Why we are building this
- The server
- Traefik and SSL, you are here
- The first Supabase instance
- Vault
- Two instances
- Security and the load test
Top comments (0)