Docker Homelab
Isometric diagram of a network globe and padlock cabled to a white reverse proxy box that routes traffic to three stacked containers and a server.
networking

How to Set Up Traefik with Docker Compose (HTTP and HTTPS)

A step-by-step guide to running Traefik as a reverse proxy with Docker Compose: HTTP routing, automatic HTTPS via Let's Encrypt, and dashboard security.

By Docker Homelab Editorial · ·Updated August 18, 2026 · 7 min read

This is how to set up Traefik with Docker Compose so that all your containers get clean domain-based routing instead of :8080, :9000, :5000 on every service. Traefik v3 reads Docker labels directly, which means you add routing rules in the same docker-compose.yml file as the service itself. If you would rather click through a UI than write labels, Nginx Proxy Manager is the alternative. No separate config file to maintain, no reload required when you add a new container.

The guide covers two setups: a local HTTP proxy you can run today on any machine, and a production HTTPS config with automatic Let’s Encrypt certificates.

Who This Is For (And Who Should Skip It)

This is for homelab operators who already know their way around docker-compose up and want to stop juggling port numbers. If you have two or more services and a domain name, Traefik is worth the hour it takes to set up.

Skip Traefik if: you have one service and localhost:8080 is fine. It is overhead you do not need.

Prerequisites for the HTTPS section: a domain name with DNS you control, a public IP, and ports 80 and 443 open. Let’s Encrypt needs to reach your server from the internet for the TLS challenge. If you are behind CGNAT or a strict firewall, use the DNS challenge instead (covered in the Traefik DNS Challenge docs).

Part 1: Basic HTTP Setup

This configuration routes traffic by hostname on port 80. Good for local use, a private network, or as a starting point before adding TLS.

Create a project directory and a docker-compose.yml:

services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--api.insecure=true"
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
      - "traefik.http.routers.whoami.entrypoints=web"

Start it:

docker compose up -d

The Traefik dashboard is at http://localhost:8080/dashboard/. The whoami service responds at http://whoami.localhost if your system resolves .localhost names (most Linux systems do). From another machine on your network, add a hosts file entry or point a real subdomain at the server’s IP.

Two label lines do all the routing work:

  • traefik.enable=true — you must opt each service in explicitly because exposedbydefault=false is set. This is the right default: it prevents Traefik from accidentally exposing containers you did not intend to route.
  • traefik.http.routers.whoami.rule=Host(...) — the routing rule. You can combine conditions: Host('app.example.com') && PathPrefix('/api') is valid.

The Docker socket mount (:ro for read-only) is how Traefik discovers containers as they start and stop. Traefik does not need write access to the socket. The read-only flag limits the blast radius if something goes wrong.

Part 2: HTTPS with Automatic Let’s Encrypt Certificates

Once you have a real domain and a publicly reachable server, replace the HTTP-only config with this:

services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.email=you@example.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt

  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=myresolver"

Replace you@example.com with a real email (Let’s Encrypt sends expiry warnings to it) and whoami.example.com with your actual subdomain. The ./letsencrypt volume persists certificates across container restarts. Everything about the shared network it rides on is covered in our Docker Compose networking guide. Without it, Traefik requests a fresh certificate every time it starts, which will hit Let’s Encrypt’s rate limits quickly.

A permissions gotcha worth knowing before it bites you: many guides mount acme.json as a single bind-mounted file rather than mounting a directory. If you do it that way, the file must be chmod 600 — Traefik refuses to start outright when the certificate store has looser permissions, and the resulting error is easy to misread as a config problem. Mounting the whole ./letsencrypt directory as above sidesteps it, because Traefik creates acme.json itself with the permissions it wants.

If you are on a local-only network with no public domain, you do not have to skip TLS entirely: a wildcard DNS service such as nip.io, or self-signed certificates, will get you a working HTTPS path for testing. The configuration above assumes the public route.

Before going live, test against the Let’s Encrypt staging environment by adding this line to the command block:

- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"

Staging certificates are not trusted by browsers but they do not count against production rate limits. Once the staging request succeeds, remove that line, delete ./letsencrypt/acme.json, and restart Traefik to get a real certificate.

Adding HTTP to HTTPS Redirect

Without an explicit redirect, traffic hitting port 80 will receive a connection refused or time out, not a redirect to HTTPS. Add a redirect middleware to handle this cleanly:

    command:
      # ... other args ...
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"

This tells the web (port 80) entrypoint to issue a 301 redirect to the websecure (port 443) entrypoint for all incoming requests. No per-service label is required.

Shared Network for Multi-Stack Setups

When services live in separate docker-compose.yml files, they are on different Docker networks by default. Traefik cannot route to a container it cannot reach on the network layer. The fix is a shared external network:

docker network create traefik-public

Add this to every docker-compose.yml that needs routing:

networks:
  traefik-public:
    external: true

services:
  traefik:
    networks:
      - traefik-public

  myapp:
    networks:
      - traefik-public
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls.certresolver=myresolver"
      - "traefik.docker.network=traefik-public"

The traefik.docker.network label is important when a container is attached to multiple networks. Without it, Traefik may pick the wrong interface to forward traffic through.

Services That Listen on a Non-Standard Internal Port

Traefik assumes the container serves on port 80 unless told otherwise. Plenty of self-hosted apps do not: Jellyfin listens on 8096 inside its container, and many others pick their own. When the internal port is anything but 80, add a loadbalancer.server.port label:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(`media.example.com`)"
      - "traefik.http.routers.jellyfin.entrypoints=websecure"
      - "traefik.http.routers.jellyfin.tls.certresolver=myresolver"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"

Notice what is absent from that service: a ports: section. Once a container is routed by Traefik it should not publish ports to the host at all. Traefik reaches it over the shared Docker network, so the container becomes unreachable except through the proxy — which is the point, and a meaningful reduction in exposed surface. Every additional service after the first follows the same shape: join the shared network, add the four routing labels, set the internal port if it isn’t 80, delete ports:. A good first candidate is Jellyfin. Traefik itself needs no changes.

Dashboard Security in Production

The --api.insecure=true flag used in the basic example exposes the Traefik dashboard on port 8080 with no authentication. That is fine on a local machine, not acceptable on anything internet-facing.

For a server with a public IP, either block port 8080 at your firewall, or remove --api.insecure=true and serve the dashboard through Traefik itself. Swap the insecure flag for --api.dashboard=true in the command block, then give the dashboard its own router pointed at Traefik’s internal API service:

    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=myresolver"
      - "traefik.http.routers.dashboard.service=api@internal"

api@internal is the built-in service name for Traefik’s own API; you do not define it yourself. Put authentication in front of that router before it goes anywhere public — the dashboard exposes your entire routing configuration, which is a map of everything you run. The Traefik middleware docs cover the basicauth middleware configuration, and IP allowlisting is a reasonable second layer. A Tailscale tunnel is another option if you want dashboard access only from devices on your tailnet without punching a hole in your firewall.

Operations

Traefik picks up new containers automatically when they start. You do not need to restart Traefik when you add a new service with correct labels. The only time Traefik needs a restart is when you change its own command arguments (static configuration).

For updates, pin the image tag (traefik:v3.3) rather than using latest. Traefik v2 to v3 had breaking label changes, so an uncontrolled update could break routing across all your services at once. Check the Traefik migration guide before moving between major versions.

Certificate renewal is automatic. Traefik checks and renews Let’s Encrypt certificates before they expire, provided the container is running and port 80 or 443 is reachable for the ACME challenge.

When a route does not come up

Two commands resolve most of it. Traefik’s own log usually names the problem outright, including certificate failures and rejected labels:

docker logs traefik -f

If the log is quiet about a service, the container is probably not on the network Traefik is watching. Confirm it directly:

docker inspect myapp | grep -A 5 Networks

A container missing from the shared network is the single most common cause of a route that never appears, and it produces no error anywhere — Traefik simply never sees the container. The second most common cause is a service running with network_mode: host, which cannot join a Docker network at all and therefore cannot be discovered by label; Home Assistant and Pi-hole are the usual candidates. Those have to be routed as fixed upstreams pointing at the host’s address, for the reasons set out in the Docker Compose host networking guide. Expect a few seconds of delay on the very first request to a new hostname, too, while the certificate is issued; that pause is normal and does not repeat.

Sources

  1. Traefik Docker Quick Start (Official Docs)
  2. Docker Compose Basic Example - Traefik v3.3
  3. HTTP Routing with Traefik - Docker Docs
  4. Docker Compose with Let's Encrypt TLS Challenge - Traefik
#traefik #docker-compose #reverse-proxy #self-hosted #tls#letsencrypt#https

Related