Why a Reverse Proxy Changes How You Run Self-Hosted Services
Running multiple services on a single server without a reverse proxy means juggling port numbers constantly – :8080 for one app, :3000 for another, :9000 for something else. It works, but it breaks down the moment you want clean domain names, HTTPS, or any kind of centralized access control. A reverse proxy sits in front of all those services and handles routing, so you get app.yourdomain.com instead of 192.168.1.10:8080. Traefik takes that idea further by watching your Docker containers directly and configuring itself automatically as services start and stop.
Unlike Nginx or Caddy, which require you to write and reload configuration files every time you add a service, Traefik reads Docker labels attached to your containers and builds routing rules on the fly. Add a container with the right labels, and Traefik picks it up within seconds. It also handles SSL certificates through Let’s Encrypt automatically, including renewals. For anyone running a home lab or small self-hosted stack, that combination of zero-reload routing and automatic HTTPS removes a significant layer of ongoing maintenance.

Prerequisites and What You Will Need
Before writing a single line of configuration, you need a few things in place. A Linux server or VPS with Docker and Docker Compose installed is the baseline. You also need a domain name with DNS records pointing to your server’s public IP – Traefik can handle local-only setups, but automatic HTTPS through Let’s Encrypt requires a publicly reachable domain. For this guide, the assumption is that you control DNS and have at least one A record ready to use.
Create a dedicated Docker network for Traefik to use. This keeps your proxy traffic separate from inter-container communication and lets Traefik discover services without every container being on the same default bridge network. Run docker network create traefik-proxy before doing anything else. You will reference this network name throughout the configuration, so pick something consistent and stick with it.
You will also need an email address for Let’s Encrypt certificate registration. Let’s Encrypt sends expiration notices to this address and uses it to identify certificate requests. Create a file at /opt/traefik/acme.json on your host and set its permissions to 600 with chmod 600 acme.json – Traefik will refuse to start if this file has broader permissions, treating it as a security risk since it stores your private certificate data.
Writing the Core Traefik Configuration
Traefik’s configuration splits into two layers: static config, which defines entrypoints, providers, and certificate resolvers, and dynamic config, which defines routers and services and is mostly handled through Docker labels. The static config lives in a file called traefik.yml or gets passed via command-line arguments in your Compose file. Using a dedicated config file is cleaner for anything beyond a minimal setup.
Here is a working traefik.yml to start with:
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirects:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik-proxy
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme.json
httpChallenge:
entryPoint: web
The exposedByDefault: false line is worth paying attention to. With it set to false, Traefik ignores every Docker container unless you explicitly opt it in with a label. This prevents accidental exposure of internal services – databases, admin panels, or anything else running in Docker that you never intended to route traffic to. Every service you want Traefik to handle needs the label traefik.enable=true, which acts as a deliberate whitelist.

Deploying Traefik with Docker Compose
With the config file written, the Docker Compose setup for Traefik itself looks like this:
version: "3.8"
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.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=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."
networks:
traefik-proxy:
external: true
The Docker socket is mounted read-only, which limits what Traefik can do with it. Writing to the socket is not needed for service discovery, and read-only is a meaningful security boundary in environments where container breakout is a concern. Replace traefik.yourdomain.com with whatever subdomain you want for the dashboard. The basicauth middleware on the dashboard router is not optional – an exposed Traefik dashboard without authentication gives anyone with a browser a full map of your running services and routing rules. Generate a bcrypt password hash with htpasswd -nb admin yourpassword and paste the output into the label, escaping each $ with $$ as shown.
Start Traefik with docker compose up -d from the directory containing your Compose file. Check logs with docker logs traefik -f and watch for certificate issuance messages from Let’s Encrypt. The first certificate request can take up to a minute. If you see errors about acme.json permissions or unreachable ACME endpoints, those are almost always the file permission issue or a DNS record that hasn’t propagated yet.
Once Traefik is running, adding any other service to the proxy is a matter of attaching the right labels. A self-hosted bookmarking tool like Hoarder, for example, only needs the network assignment and four labels: enable, router rule, entrypoint, and cert resolver. No config file edits, no Traefik restart. The container comes up, Traefik detects it, and the route is live.

One area that catches people off guard is middleware ordering and scope. Traefik middlewares are defined at the router level, not the service level, which means the same middleware definition can be reused across multiple routers or scoped tightly to a single one. If you define a rate-limiter or IP allowlist middleware on one router, it does not automatically apply to anything else – you have to explicitly attach it. This is useful when you want public-facing services to have stricter rules than internal ones, but it also means a misconfigured label on one container won’t silently break another. Each router operates in its own lane, which makes debugging considerably more predictable than flat Nginx config files where a misplaced location block can affect unrelated routes.
The wildcard certificate option is worth exploring once your basic setup is stable. Instead of issuing individual certificates per subdomain, Traefik can request a single wildcard cert for *.yourdomain.com using a DNS challenge rather than the HTTP challenge shown above. This requires a DNS provider with API support and a corresponding Traefik DNS challenge plugin, but it means every new subdomain you spin up gets covered automatically without waiting for a new certificate request cycle. The trade-off is that DNS API credentials need to be passed to Traefik as environment variables, adding a credential management consideration to your setup that the simpler HTTP challenge avoids entirely.





