Tutorial

Deployment

Deploy your applications with Traefik on a VPS

Traefik is a cloud-native reverse proxy and load balancer that discovers your Docker containers automatically and assigns them routes and SSL certificates without manual reloading. Deployed on your VPS, it becomes the single entry point for all your applications, with a declarative configuration through Docker labels. This guide installs the v3 branch (v3.7 at the time of writing), with Let's Encrypt HTTPS, a protected dashboard, HTTP/3, production middlewares and the pitfalls to know if you are coming from Traefik v2.

11 min read7 steps
Contents· Why put Traefik in front of your VPS?1/13
  1. 01Why put Traefik in front of your VPS?
  2. 02What Traefik brings concretely on a VPS
  3. 03Prerequisites
  4. 04Deploy Traefik with Docker Compose
  5. 05Useful middlewares in production
  6. 06Five middlewares to know
  7. 07Enable HTTP/3 (QUIC)
  8. 08Coming from Traefik v2: what breaks
  9. 09v3 breaking changes
  10. 10Traefik v3, Nginx or Caddy in front of your containers
  11. 11Troubleshooting: the most frequent errors
  12. 12Symptoms and fixes
  13. 13Next step: harden the stack

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

  1. 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.json file (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.json

    Create 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.

  2. Configure the entrypoints and the ACME resolver

    Place this file in /opt/traefik/docker-compose.yml. It declares two entrypoints (web on :80, websecure on :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: true

    The exposedbydefault=false option is essential: without it, every container on the VPS would be published, including your database. Pin the branch (v3.7) rather than latest, and follow the releases before moving to a new minor version.

  3. Force the HTTP to HTTPS redirection

    The redirection is declared directly on the web entrypoint, with the two options --entrypoints.web.http.redirections.entrypoint.to=websecure and ...entrypoint.scheme=https already 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.

  4. Secure the dashboard

    The dashboard is exposed on traefik.example.com, behind the basicauth middleware named dashboard-auth. Generate a bcrypt hash (the apache2-utils package 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=true flag in production: it publishes the dashboard without authentication.

  5. Launch Traefik

    Start the stack and follow the logs:

    cd /opt/traefik
    docker compose up -d
    docker compose logs -f traefik

    You should see the Docker provider start, then the dashboard router 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 empty acme.json before going to production.

  6. Connect an application

    Each application lives in its own docker-compose.yml, attached to the proxy network 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: true

    On docker compose up -d, Traefik detects the container, obtains the certificate and activates the route within seconds, without touching its own configuration. The loadbalancer.server.port label gives the application's internal port; it becomes mandatory as soon as the image exposes several. The router name (app here) must be unique across the whole VPS.

  7. 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.com

    The first request must return a permanent redirect to https://, the second a 200 (or your application's code) served with a valid Let's Encrypt certificate, and dig must 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$$..., then traefik.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=1s and ...ratelimit.burst=200 allow 100 requests per second on average, with bursts up to 200. Without average, the limit is disabled.
  • Security headers: the headers middleware sets HSTS (stsSeconds=31536000, stsIncludeSubdomains=true), X-Frame-Options: DENY (frameDeny=true) and X-Content-Type-Options: nosniff (contentTypeNosniff=true), with no separate configuration file.
  • Redirect www to the bare domain: redirectregex with regex=^https://www\.(.+), replacement=https://$${1} and permanent=true. Remember to declare both hosts in the router rule so that the certificate covers www.
  • Compression: traefik.http.middlewares.compress.compress=true enables 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 swarmMode option no longer exists and blocks startup. Docker Swarm now has its own provider (--providers.swarm).
  • Routing rules: Headers and HeadersRegexp become Header and HeaderRegexp, HostHeader disappears in favor of Host, and most matchers now accept a single value.
  • Paths: Path and PathPrefix no longer read a regular expression or a placeholder such as {id}; such a route no longer matches anything. Switch to PathRegexp, which follows Go regexp syntax, like HostRegexp.
  • Renamed or trimmed middlewares: IPWhiteList becomes IPAllowList (same options); the headers middleware loses sslRedirect, sslTemporaryRedirect, sslHost, sslForceHost and featurePolicy.
  • Content-Type: Traefik no longer guesses the type of a response the backend did not type; add the ContentType middleware 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

CriterionTraefik v3NginxCaddy 2
ConfigurationDocker labels, YAML or TOML, applied on the flyStatic vhost files, `nginx -s reload` on every changeCaddyfile or JSON API, reload without downtime
Automatic HTTPSBuilt in (ACME: HTTP-01, TLS-ALPN-01, DNS-01)External tool (certbot) and a renewal jobBuilt in and on by default
Container discoveryNative, through the Docker socketNone: third-party tool or manual editingThrough the community caddy-docker-proxy module
HTTP/3Stable since v3.0 (April 2024), one option per entrypointExperimental since nginx 1.25.0 (May 2023), dedicated configurationOn by default since Caddy 2.6 (September 2022)
Learning curveModerate: routers, services and middlewares to learnKnown by everyone, but HTTPS + Docker need toolingThe 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 unauthorized or 403 error 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 the dnsChallenge.
  • permissions 644 for /acme.json are too open, please use 600: the resolver refuses the file. Fix it with chmod 600 /opt/traefik/acme.json then restart Traefik; this check protects the private keys stored inside.
  • Route visible in the dashboard but 502 Bad Gateway: the container is not on the proxy network, or the internal port is wrong. Check with docker inspect <container> (Networks section), add the network or the loadbalancer.server.port label, then recreate the container. On a container attached to several networks, the traefik.docker.network=proxy label removes the ambiguity.
  • Labels ignored, route missing from the dashboard: traefik.enable=true is missing (mandatory with exposedbydefault=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 the docker-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.

Launch Traefik in minutes on a ServOrbit Cloud VPS

Order a Cloud VPS with Docker preinstalled and deploy your Traefik stack from the Deployment template. Your applications are routed over HTTPS automatically.