How to Install Traefik as a Reverse Proxy
Install Traefik v3 as a Docker-based reverse proxy with automatic Let's Encrypt TLS, static and dynamic config, and label-driven service routing.
Before you start
- ▸Docker Engine 24+ and Docker Compose v2 installed
- ▸A domain name with DNS A records pointing to your server
- ▸Ports 80 and 443 open in your firewall
- ▸Root or sudo access on the host
Traefik is a cloud-native reverse proxy and load balancer that integrates directly with Docker, Kubernetes, and other orchestrators to discover services automatically. Unlike Nginx or Caddy configs you hand-write for every service, Traefik reads labels on your containers and updates its routing table on the fly. This guide walks through a production-ready Traefik v3 setup: static config, dynamic routing, Let's Encrypt TLS via ACME, and exposing Docker-based services.
Architecture Overview
Traefik separates configuration into two layers:
- Static config – entrypoints, providers, certificate resolvers. Set once at startup via
traefik.ymlor CLI flags. - Dynamic config – routers, services, middlewares. Updated at runtime from providers (Docker labels, file provider, etc.).
All examples below run Traefik itself as a Docker container managed by docker compose. The host runs a modern systemd-based distro with Docker Engine installed.
Prerequisites
- Docker Engine 24+ and Docker Compose v2 (
docker compose, notdocker-compose). - A domain name with DNS A records pointing to your server's public IP.
- Ports 80 and 443 open in your firewall.
Open firewall ports
Pick the tool matching your distro.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
firewalld (Fedora/RHEL/Rocky):
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
nftables (Arch or manual setup):
sudo nft add rule inet filter input tcp dport { 80, 443 } accept
Step 1 – Create the Project Directory
Keep all Traefik config in one place:
mkdir -p ~/traefik/{config,certs}
cd ~/traefik
The certs/ directory will hold the ACME JSON file. It must be locked down:
touch certs/acme.json
chmod 600 certs/acme.json
Step 2 – Write the Static Configuration
Create config/traefik.yml. This file is read once at container startup.
cat > config/traefik.yml << 'EOF'
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /certs/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
network: traefik_proxy
file:
directory: /config/dynamic
watch: true
log:
level: INFO
EOF
Key decisions here:
exposedByDefault: falsemeans containers are ignored until you add a label — safer for production.httpChallengeworks for most setups. Switch todnsChallengeif your server isn't publicly reachable on port 80 (see Troubleshooting).- The file provider watches
/config/dynamic/for hot-reloaded dynamic config files.
Step 3 – Create the Docker Network
All proxied containers must share a network with Traefik:
docker network create traefik_proxy
Step 4 – Write the Docker Compose File
cat > docker-compose.yml << 'EOF'
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik.yml:/etc/traefik/traefik.yml:ro
- ./config/dynamic:/config/dynamic:ro
- ./certs:/certs
networks:
- traefik_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=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xyz$$hashedpasswordhere"
networks:
traefik_proxy:
external: true
EOF
Replace traefik.example.com with your actual subdomain. Generate a bcrypt/apr1 password hash for the dashboard basic auth:
# Install htpasswd if needed:
# Ubuntu/Debian: sudo apt install apache2-utils
# Fedora/RHEL: sudo dnf install httpd-tools
# Arch: sudo pacman -S apache
htpasswd -nb admin yourpassword
Copy the output (escape any $ signs as $$ in the Compose label).
Step 5 – Add Dynamic Configuration
Create the dynamic directory and an optional middleware snippet for extra security headers:
mkdir -p config/dynamic
cat > config/dynamic/middlewares.yml << 'EOF'
http:
middlewares:
secure-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
EOF
Traefik picks this up live — no restart needed.
Step 6 – Start Traefik
docker compose up -d
docker compose logs -f traefik
Watch the logs. You should see Traefik start, contact Let's Encrypt, and complete the HTTP-01 challenge. Logs will include lines like Obtaining ACME certificate and Certificate obtained successfully.
Step 7 – Route a Docker Service
Add labels to any container to expose it through Traefik. Here's a minimal example using Whoami as a test service:
cat > whoami-compose.yml << 'EOF'
services:
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
networks:
- traefik_proxy
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=letsencrypt"
- "traefik.http.middlewares.whoami.headers.customrequestheaders.X-Forwarded-Proto=https"
networks:
traefik_proxy:
external: true
EOF
docker compose -f whoami-compose.yml up -d
Traefik discovers the container immediately and begins serving https://whoami.example.com with a fresh TLS certificate.
Verification
Check the Traefik dashboard at https://traefik.example.com — you should see the whoami router listed as active. From the command line:
curl -I https://whoami.example.com
Expected: HTTP/2 200 with valid TLS. Also verify the HTTP→HTTPS redirect:
curl -I http://whoami.example.com
Expected: 301 Moved Permanently pointing to the HTTPS URL.
Inspect currently active certificates:
cat certs/acme.json | python3 -m json.tool | grep '"domain"'
Troubleshooting
ACME certificate never issues
- Confirm port 80 is reachable from the internet:
curl http://yourdomain.comfrom an outside machine. - Check
acme.jsonpermissions are exactly600; Traefik refuses to write certificates otherwise. - If the server is behind NAT or a CDN, switch to
dnsChallengeintraefik.ymland configure your DNS provider's API credentials as environment variables.
Container not appearing in Traefik
- Confirm the container is on the
traefik_proxynetwork:docker inspect <container> | grep -A5 Networks. - Ensure
traefik.enable=truelabel is set — rememberexposedByDefault: false. - Verify the network name in
traefik.ymlmatches exactly:traefik_proxy.
Dashboard returns 404
The service=api@internal label is required for the dashboard router. Without it Traefik tries to find a container service named dashboard and fails.
Stale certificates after domain change
Delete and recreate acme.json, then restart Traefik. Keep a backup first — losing the file mid-renewal causes a short outage.
Frequently asked questions
- Can I use Traefik without Docker — with plain systemd services?
- Yes. Use the file provider exclusively: define routers and services in YAML files under config/dynamic pointing to localhost ports. Install the Traefik binary directly and run it as a systemd service using an ExecStart pointing to your traefik.yml.
- How do I use a wildcard certificate instead of per-domain certificates?
- Switch from httpChallenge to dnsChallenge in the ACME config and set the router rule to HostRegexp or use a wildcard Host(`*.example.com`). Wildcard certs require DNS-01 challenge; configure your DNS provider's API token as an environment variable per Traefik's provider docs.
- Is mounting the Docker socket a security risk?
- Yes — access to the Docker socket is effectively root on the host. Mitigate it by mounting the socket read-only (:ro), setting exposedByDefault: false, and running Traefik behind a dedicated user namespace. For stricter setups consider using the Traefik Proxy provider via TCP socket proxy (docker-socket-proxy).
- How do I add path-based routing instead of host-based?
- Change the router rule label to use PathPrefix: traefik.http.routers.myapp.rule=Host(`example.com`) && PathPrefix(`/app`). Add a StripPrefix middleware if your backend doesn't expect the path prefix.
- What happens when Let's Encrypt rate-limits me during testing?
- Use Let's Encrypt's staging server by adding caServer: https://acme-staging-v02.api.letsencrypt.org/directory to the acme block in traefik.yml. Staging certificates are invalid for browsers but let you test the full ACME flow without hitting production rate limits.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.