Tutorial

Traefik v3 on VPS: Docker Reverse Proxy with Automatic HTTPS

Deployment9 min read9 steps

You have three Docker containers on your VPS: a Node app, a Python backend, an admin tool. Each one listens on a different port, you manage certificates manually, and every new service means editing nginx and reloading the config. Traefik v3 solves exactly this: it reads Docker labels from your containers, configures routes in real time, autonomously renews Let's Encrypt certificates, and exposes a monitoring dashboard — without a single line of nginx configuration.

Contents· Why Traefik v3 over nginx for your Docker stack1/8
  1. 01Why Traefik v3 over nginx for your Docker stack
  2. 02Technical prerequisites before you start
  3. 03Complete Traefik v3 installation step by step
  4. 04Essential middlewares: BasicAuth, rate-limit and security headers
  5. 05Migrating from Traefik v2: breaking changes
  6. 06Traefik v3 vs nginx vs Caddy: which reverse proxy for Docker?
  7. 07Troubleshooting: the five most common errors
  8. 08Next step: harden your Docker stack for production

Why Traefik v3 over nginx for your Docker stack

nginx is an excellent HTTP server, but its configuration is static: every new service requires a new vhost file, a process reload and often a manual update to ACME renewal. Traefik was built for containerized environments where topology changes constantly. In version 3, it takes this further with native HTTP/3 (QUIC) support, a simplified middleware syntax, and a stricter runtime that rejects ambiguous configurations.

  • Automatic container discovery via Docker labels — no manual reload when you add a service.
  • Automatic HTTPS with Let's Encrypt (TLS-ALPN-01 or HTTP-01 challenge): certificates obtained, stored and renewed without intervention.
  • Native middlewares declarable by label: BasicAuth, rate-limit, security headers, redirects — no nginx module to write.
  • Built-in real-time dashboard: view all routes, middlewares and certificate status from a browser.
  • HTTP/3 (QUIC) support in a single config line: improves performance on high-latency connections.
  • Routing by Host, PathPrefix, Headers or any combination — rules are written in expressive Go template syntax.
  • Internal REST API: query router state, inspect active backends, automate CI/CD integrations.
  • Compatible with Docker Swarm, Kubernetes and Nomad — migrate without changing your labels.

Technical prerequisites before you start

This installation requires a VPS running Debian 12 or Ubuntu 22.04/24.04 (commands are identical), Docker Engine 25+ and Docker Compose v2.24+ (the docker compose command without a hyphen). Check that ports 80 and 443 are open inbound from the Internet in your firewall. You need a domain name whose DNS you control: point an A record to your VPS IP before starting, otherwise Let's Encrypt cannot issue the certificate. A valid email address is required for ACME expiry notifications.

Complete Traefik v3 installation step by step

  1. Create the shared Docker network. Traefik must be on the same network as the services it proxies. Create it once: docker network create traefik-public. This network will be referenced in every service docker-compose.yml.

  2. Create the acme.json file for certificates. Let's Encrypt will write its certificates into this file. It must be owned by root with strictly 600 permissions: touch /opt/traefik/acme.json && chmod 600 /opt/traefik/acme.json. If permissions are wrong, Traefik will refuse to start.

  3. Create the Traefik docker-compose.yml. Place this file at /opt/traefik/docker-compose.yml with the full configuration including TLS entrypoints, the Let's Encrypt ACME resolver, and the protected dashboard labels.

  4. Generate the BasicAuth hash for the dashboard. Replace the hardcoded string with a real bcrypt hash: echo $(htpasswd -nB admin) | sed -e 's/\$/\$\$/g'. The htpasswd command is in the apache2-utils package on Debian/Ubuntu. Dollar signs must be doubled ($$) in Docker Compose label values.

  5. Start Traefik. From /opt/traefik/: docker compose up -d. Check logs: docker compose logs -f traefik. You should see "Starting provider" providerType=Docker then "Attaching router" routerName=dashboard. An error on acme.json means wrong permissions.

  6. Add a service behind Traefik. In your application's docker-compose.yml, join the traefik-public network (as external) and add the four key labels: traefik.enable=true, the Host() rule, the websecure entrypoint, the letsencrypt certresolver, and the backend port. Traefik detects the container immediately after docker compose up -d.

  7. Verify Let's Encrypt certificates. Inspect acme.json: cat /opt/traefik/acme.json | python3 -m json.tool | grep -A3 'domain'. You should see your domains listed with an expiry 90 days out. Alternatively, the Traefik dashboard lists all active certificates under the *Routers* tab.

  8. Validate the HTTP→HTTPS redirect. Test with curl: curl -v http://app.your-domain.com. You must receive a 301 Moved Permanently to https://. A connection refused on port 80 means firewall rules are blocking the port.

  9. Enable HTTP/3 on the browser side. Add the Alt-Svc header via a global middleware in your Traefik labels or a static traefik.yml config file. The header Alt-Svc: h3=":443"; ma=86400 tells the browser your server supports QUIC. Verify with curl --http3 https://app.your-domain.com -v.

Essential middlewares: BasicAuth, rate-limit and security headers

Traefik middlewares are applied via Docker labels directly on the service router. Here are the three most useful middlewares in production, declared on the service itself without touching Traefik's central configuration.

  • BasicAuth on a private route — to protect an admin tool (Adminer, Flower, etc.): add traefik.http.middlewares.adminauth.basicauth.users=admin:$$hash$$ and traefik.http.routers.myadmin.middlewares=adminauth labels on the service.
  • Rate-limit against abuse — protects an API or contact form: set ratelimit.average=100 (100 req/s average) and ratelimit.burst=50 (burst of 50 tolerated), then apply the middleware on the router.
  • HTTP security headers — X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security: Traefik's headers middleware accepts all these values directly as labels, no separate nginx config file needed.
  • Redirect Regex — to redirect www.your-domain.com to your-domain.com: use the redirectregex middleware with permanent=true and a regex ^https://www\.(.+) → https://${1}.
  • Compress — enable gzip/brotli compression on all text responses with a single label: traefik.http.middlewares.compress.compress=true. Apply globally by adding it as a default middleware in the static configuration.

HTTP/3 (QUIC) in two lines. In the command section of Traefik's docker-compose.yml, add --entrypoints.websecure.http3=true and expose UDP port 443 (443:443/udp). QUIC establishes connections up to 30% faster on lossy networks and eliminates TLS-over-TCP head-of-line blocking. Modern browsers (Chrome 87+, Firefox 88+, Safari 16.4+) activate it automatically after the first exchange via the Alt-Svc header. No application-side changes are needed.

Migrating from Traefik v2: breaking changes

Traefik v3 introduces intentional breaks with v2 to clean up accumulated ambiguities. The good news: migration is progressive — v2 and v3 configurations can coexist during the transition. Here are the most frequently breaking points.

  • docker.swarmMode renamed to docker.swarmmode (all lowercase). If your v2 config uses the old casing, Traefik v3 silently ignores it and Swarm discovery doesn't work.
  • PathPrefix rule without trailing slash: in v3, PathPrefix('/api') is equivalent to PathPrefix('/api/') — the trailing slash is normalized. If you had rules that depended on this behavior to distinguish /api from /apiv2, revise them.
  • tls.certificates in dynamic configuration: in v2, you could define manual certificates directly in a file provider's dynamic config. In v3, this section must be in the static configuration (traefik.yml) or via a dedicated provider.
  • errors middleware: the query parameter is renamed to url. Any config using errors.query must be updated to errors.url.
  • Removal of spiffe protocol support: if you used SPIFFE authentication in a Consul infrastructure, migrate to a standard mTLS solution before upgrading to v3.
  • Docker constraints configuration: the constraints syntax has changed to align with the standard Docker tag format. Check your constraint list if you were filtering services by tag.

Traefik v3 vs nginx vs Caddy: which reverse proxy for Docker?

Scroll the table

CriterionTraefik v3nginxCaddy v2
ConfigurationDocker labels, TOML or YAML — automatic hot reloadStatic vhost files — manual reload (`nginx -s reload`)Declarative Caddyfile — automatic hot reload
Automatic HTTPSNatively built-in (ACME v2, TLS-ALPN or HTTP-01 challenge)Requires certbot + cron + reload scriptNatively built-in, simplest to configure (one line)
Dynamic Docker routingNative: real-time container discovery via Docker socketNot native: requires nginx-proxy or manual modificationNative via caddy-docker-proxy plugin (unofficial)
HTTP/3 (QUIC)Natively supported since v3 (2024), one-line activationSupported since nginx 1.25 (2023), extra configuration neededNatively supported since Caddy 2.6 (2022)
Raw performanceSlightly below nginx at very high throughput (>50k req/s)Industry reference for high throughput and concurrencyComparable to nginx for most standard web workloads
Learning curveModerate — router/service/middleware concepts to understandHigh for HTTPS + Docker — fragmented documentationLow — most concise configuration for simple cases

Troubleshooting: the five most common errors

The majority of Traefik production issues come from three sources: acme.json file permissions, missing Docker network, and incorrectly escaped labels in Compose. Here are the errors with their actual messages.

  • level=error msg="Unable to obtain ACME certificate" error="acme: error 403" or unauthorized — Let's Encrypt can't verify your domain. Check that port 80 is open from the Internet and that your domain's DNS points to your VPS IP.
  • level=error msg="The acme.json file has bad file permissions" then Traefik refuses to start — file permissions are not 600. Fix: chmod 600 /opt/traefik/acme.json. This check is intentionally blocking: a world-readable file would expose your private keys.
  • Service visible in Traefik dashboard but 502 Bad Gateway — the service container is not on the traefik-public network. Check with docker inspect <container> | grep -A20 Networks. Add the network in the service's docker-compose.yml and recreate the container.
  • Labels silently ignored, route absent from dashboard — you forgot traefik.enable=true on the service, or --providers.docker.exposedbydefault=false is active (recommended) and the label is missing. Also check that the router name in traefik.http.routers.<name>.rule is unique across all your services.
  • invalid character '$' in label value — dollar signs in BasicAuth hashes are not doubled in the Compose file. The correct syntax is $$2y$$10$$... (each $ becomes $$) when the value is in a docker-compose.yml. If you pass the value via an environment variable, a single $ is enough.

Next step: harden your Docker stack for production

Traefik v3 handles routing and TLS, but a production-ready Docker stack requires more: healthchecks on containers, restart policy, centralized logs, secret rotation, volume backups. See our docker-compose-production-checklist guide for a complete checklist to apply before going live with any containerized service on your VPS.

Deploy your infrastructure today

VPS Cloud. Preconfigured environments available.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab