Why put Traefik in front of your VPS?
When you run several containerized applications on the same VPS, manually managing Nginx virtual hosts and renewing each certificate quickly becomes unmanageable. Traefik solves this problem by reading your Docker containers' labels directly: you declare a route at docker run time or in your docker-compose.yml, and Traefik configures it on the fly, generates the Let's Encrypt certificate and exposes it over HTTPS. No config file to edit, no reload: everything is dynamic.
Nginx remains an excellent HTTP server, but its configuration is static: each new service needs a vhost, an nginx -s reload and a separate ACME tool (certbot and its renewal job). Traefik was designed for a topology that keeps changing, which is exactly the case of a VPS where containers are added, updated and removed every week. The v3 branch, released in April 2024, stabilized HTTP/3 and OpenTelemetry trace export, and tightened the routing rule syntax.
What Traefik brings concretely on a VPS
- Automatic discovery of Docker containers via the Docker provider (routes appear without a restart)
- Let's Encrypt SSL certificates generated and renewed automatically (HTTP-01, TLS-ALPN-01 or DNS-01 challenges), renewed by default 30 days before they expire
- Routing by domain, by path or by header, managed entirely through Docker labels
- Built-in web dashboard to visualize routers, services and middlewares in real time
- Ready-to-use middlewares: HTTPS redirection, basic auth, rate limiting, security headers, compression
- Automatic load balancing when you scale a service to several replicas
- HTTP/3 (QUIC) enabled on an entrypoint with a single option
- Providers other than Docker (Swarm, Kubernetes, Nomad, files) if your infrastructure grows
Prerequisites
Traefik itself is very lightweight: 1 vCPU and 1 GB of RAM are amply enough for the proxy alone, the overhead coming from your applications behind it. Plan for a VPS running Debian 12 or Ubuntu 22.04/24.04 with Docker Engine and the Docker Compose v2 plugin installed (the docker compose command, without a hyphen), a domain name whose A/AAAA records point to the VPS IP before the first start (otherwise Let's Encrypt cannot validate the domain), and ports 80 and 443 open for inbound TCP in your firewall (UFW), plus 443 over UDP if you enable HTTP/3. Keep a valid email address for the ACME account. For wildcard certificates, you will also need API access to your DNS provider.
Deploy Traefik with Docker Compose
Create the network and the directory structure
Create a shared Docker network that Traefik and your applications will use, then a dedicated folder containing the
acme.jsonfile (the storage for certificates and their private keys):docker network create proxy mkdir -p /opt/traefik && cd /opt/traefik touch acme.json && chmod 600 acme.jsonCreate the file before the first start: if the mounted path does not exist, Docker creates a directory in its place and Traefik can no longer write to it. The 600 permissions are mandatory: Traefik refuses a file readable by other users.
Configure the entrypoints and the ACME resolver
Place this file in
/opt/traefik/docker-compose.yml. It declares two entrypoints (webon :80,websecureon :443), a Let's Encrypt certificateResolver validated through the HTTP challenge, container discovery through the Docker socket mounted read-only, and the dashboard:services: traefik: image: traefik:v3.7 container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" command: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--providers.docker.network=proxy" - "--entrypoints.web.address=:80" - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - "--entrypoints.websecure.address=:443" - "--entrypoints.websecure.http3=true" - "[email protected]" - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json" - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" - "--api.dashboard=true" volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./acme.json:/acme.json" networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)" - "traefik.http.routers.dashboard.entrypoints=websecure" - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.middlewares=dashboard-auth" - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$REMPLACER_PAR_LE_HASH" networks: proxy: external: trueThe
exposedbydefault=falseoption is essential: without it, every container on the VPS would be published, including your database. Pin the branch (v3.7) rather thanlatest, and follow the releases before moving to a new minor version.Force the HTTP to HTTPS redirection
The redirection is declared directly on the
webentrypoint, with the two options--entrypoints.web.http.redirections.entrypoint.to=websecureand...entrypoint.scheme=httpsalready present in the file above. It is simpler and safer than a catch-all router with a middleware: all cleartext traffic is switched to HTTPS by a permanent redirect, with no label to repeat on each service, and the Let's Encrypt HTTP challenge still goes through on port 80.Secure the dashboard
The dashboard is exposed on
traefik.example.com, behind thebasicauthmiddleware nameddashboard-auth. Generate a bcrypt hash (theapache2-utilspackage on Debian/Ubuntu) while doubling the$signs, as a label value in a Compose file requires:apt install -y apache2-utils htpasswd -nB admin | sed -e 's/\$/\$\$/g'Paste the result in place of
admin:$$2y$$05$$REMPLACER_PAR_LE_HASH. A hash pasted with single$signs is read by Compose as variables and breaks authentication. Never leave the--api.insecure=trueflag in production: it publishes the dashboard without authentication.Launch Traefik
Start the stack and follow the logs:
cd /opt/traefik docker compose up -d docker compose logs -f traefikYou should see the Docker provider start, then the
dashboardrouter attach, with no ACME error. During your tests, temporarily add--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory: the Let's Encrypt staging server issues certificates that browsers do not trust, but it protects you from the production rate limit if a configuration keeps failing. Remove the option and emptyacme.jsonbefore going to production.Connect an application
Each application lives in its own
docker-compose.yml, attached to theproxynetwork and described by its labels:services: app: image: ghcr.io/example/app:1.0.0 restart: unless-stopped networks: - proxy labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.entrypoints=websecure" - "traefik.http.routers.app.tls.certresolver=letsencrypt" - "traefik.http.services.app.loadbalancer.server.port=3000" networks: proxy: external: trueOn
docker compose up -d, Traefik detects the container, obtains the certificate and activates the route within seconds, without touching its own configuration. Theloadbalancer.server.portlabel gives the application's internal port; it becomes mandatory as soon as the image exposes several. The router name (apphere) must be unique across the whole VPS.Check the certificate and the redirection
Check from a machine outside the VPS:
curl -I http://app.example.com curl -I https://app.example.com dig +short app.example.comThe first request must return a permanent redirect to
https://, the second a200(or your application's code) served with a valid Let's Encrypt certificate, anddigmust show the VPS IP. The dashboard also lists each router with its certificate resolver. A port 80 that refuses the connection almost always points to the firewall.
Useful middlewares in production
A Traefik middleware is declared by label, once, then attached to one or several routers with traefik.http.routers.<router>.middlewares=<name1>,<name2>. You therefore define them on the service concerned, without touching Traefik's central configuration. Here are the ones that serve on almost every VPS.
Five middlewares to know
- BasicAuth on an admin tool (Adminer, Flower…):
traefik.http.middlewares.adminauth.basicauth.users=admin:$$2y$$05$$..., thentraefik.http.routers.myadmin.middlewares=adminauth. Same doubled-$rule as for the dashboard. - Rate limit in front of an API or a form:
traefik.http.middlewares.ratelimit.ratelimit.average=100,...ratelimit.period=1sand...ratelimit.burst=200allow 100 requests per second on average, with bursts up to 200. Withoutaverage, the limit is disabled. - Security headers: the
headersmiddleware sets HSTS (stsSeconds=31536000,stsIncludeSubdomains=true),X-Frame-Options: DENY(frameDeny=true) andX-Content-Type-Options: nosniff(contentTypeNosniff=true), with no separate configuration file. - Redirect www to the bare domain:
redirectregexwithregex=^https://www\.(.+),replacement=https://$${1}andpermanent=true. Remember to declare both hosts in the router rule so that the certificate coverswww. - Compression:
traefik.http.middlewares.compress.compress=trueenables gzip, Brotli and Zstandard depending on what the browser accepts, with no setting on the application side.
To obtain a wildcard certificate (*.example.com) and avoid exposing each subdomain, switch from the httpChallenge to the dnsChallenge: it is the only challenge that issues a wildcard. With Cloudflare, declare --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare and pass Traefik the CF_DNS_API_TOKEN variable, an API token restricted to the Zone / DNS / Edit and Zone / Zone / Read permissions. The older CF_API_EMAIL + CF_API_KEY pair also works, but it requires the global API key, which opens the whole account: prefer the token. Traefik then proves ownership of the domain through a TXT record, which works even for services not publicly exposed.
Enable HTTP/3 (QUIC)
The Compose file above already enables it: --entrypoints.websecure.http3=true on the TLS entrypoint, and the publication of port 443:443/udp. Traefik then listens over UDP on the same port as the TCP entrypoint; a connection always starts over TCP, then the browser switches to QUIC on the next round. Two conditions: only TLS routers are served over HTTP/3, and the firewall must let port 443 through over UDP (ufw allow 443/udp). If you publish a UDP port other than the entrypoint's, the http3.advertisedPort option announces the right one. To check, use a curl built with HTTP/3: curl --http3 -I https://app.example.com.
Coming from Traefik v2: what breaks
v3 keeps the v2 routing configuration compatible, but it changes the default rule syntax and refuses to start on several removed options. To migrate without rewriting everything at once, add --core.defaultRuleSyntax=v2 to the install configuration: your old rules keep working, you move routers one by one to the v3 syntax, then you remove the option. Traefik's logs flag every deprecated option still in use: read them after the first start. The changes that catch people most often:
v3 breaking changes
- Swarm: the Docker provider's
swarmModeoption no longer exists and blocks startup. Docker Swarm now has its own provider (--providers.swarm). - Routing rules:
HeadersandHeadersRegexpbecomeHeaderandHeaderRegexp,HostHeaderdisappears in favor ofHost, and most matchers now accept a single value. - Paths:
PathandPathPrefixno longer read a regular expression or a placeholder such as{id}; such a route no longer matches anything. Switch toPathRegexp, which follows Go regexp syntax, likeHostRegexp. - Renamed or trimmed middlewares:
IPWhiteListbecomesIPAllowList(same options); theheadersmiddleware losessslRedirect,sslTemporaryRedirect,sslHost,sslForceHostandfeaturePolicy. - Content-Type: Traefik no longer guesses the type of a response the backend did not type; add the
ContentTypemiddleware if an application relies on it. - Removed providers and observability: Marathon, Rancher v1, Traefik Pilot and InfluxDB v1 metrics prevent startup; tracing goes through OpenTelemetry only (no more direct export to Jaeger, Zipkin or Datadog).
Traefik v3, Nginx or Caddy in front of your containers
Scroll the table
| Criterion | Traefik v3 | Nginx | Caddy 2 |
|---|---|---|---|
| Configuration | Docker labels, YAML or TOML, applied on the fly | Static vhost files, `nginx -s reload` on every change | Caddyfile or JSON API, reload without downtime |
| Automatic HTTPS | Built in (ACME: HTTP-01, TLS-ALPN-01, DNS-01) | External tool (certbot) and a renewal job | Built in and on by default |
| Container discovery | Native, through the Docker socket | None: third-party tool or manual editing | Through the community caddy-docker-proxy module |
| HTTP/3 | Stable since v3.0 (April 2024), one option per entrypoint | Experimental since nginx 1.25.0 (May 2023), dedicated configuration | On by default since Caddy 2.6 (September 2022) |
| Learning curve | Moderate: routers, services and middlewares to learn | Known by everyone, but HTTPS + Docker need tooling | The fastest for a few sites |
Troubleshooting: the most frequent errors
Most Traefik incidents come from four sources: DNS or the firewall preventing Let's Encrypt validation, the acme.json file, a missing Docker network and badly written labels. The symptoms and their fixes:
Symptoms and fixes
- Certificate not obtained, ACME
unauthorizedor403error in the logs: Let's Encrypt cannot reach your domain. Check from the outside that port 80 answers (curl -I http://app.example.com) and that DNS points to the VPS (dig +short app.example.com). Behind a CDN in proxy mode, prefer thednsChallenge. permissions 644 for /acme.json are too open, please use 600: the resolver refuses the file. Fix it withchmod 600 /opt/traefik/acme.jsonthen restart Traefik; this check protects the private keys stored inside.- Route visible in the dashboard but
502 Bad Gateway: the container is not on theproxynetwork, or the internal port is wrong. Check withdocker inspect <container>(Networkssection), add the network or theloadbalancer.server.portlabel, then recreate the container. On a container attached to several networks, thetraefik.docker.network=proxylabel removes the ambiguity. - Labels ignored, route missing from the dashboard:
traefik.enable=trueis missing (mandatory withexposedbydefault=false), or two services declare the same router name. Traefik's logs then report the conflict. - BasicAuth refusing a correct password: the
$signs of the hash are not doubled in thedocker-compose.yml. In a Compose file, each$becomes$$; in a YAML or TOML dynamic configuration file, the hash is pasted as is.
Next step: harden the stack
Traefik handles routing and TLS, but a production-ready stack needs more: healthchecks, a restart policy, log rotation, secrets kept out of the Compose file, volume backups (including acme.json). Our Docker Compose production checklist covers these points one by one. And if you are still hesitating over the tool, the Caddy, Traefik or Nginx Proxy Manager comparison helps you decide based on the number of services and the need for automatic discovery.