How to Install Traefik on Ubuntu 24.04 VPS: Modern Reverse Proxy with Automatic Let's Encrypt
Traefik has become the default ingress controller for thousands of Docker and Kubernetes deployments because it eliminates the friction of managing config files and TLS certificates by hand. Point it at your Docker socket, add a few labels to your containers, and Traefik discovers them automatically, provisions Let's Encrypt certificates on demand, and routes traffic with zero downtime reloads. This guide walks through installing Traefik v3 on an Ubuntu 24.04 VPS, configuring HTTP-01 and Cloudflare DNS-01 challenges, hardening it with middlewares, and wiring up Authelia for single sign-on.
Looking for a simpler setup? If you prefer a GUI-driven workflow, see our Nginx Proxy Manager guide. For a classical static-config proxy, try Nginx or the batteries-included Caddy server.
Table of Contents
What is Traefik?
Traefik is an open-source edge router and reverse proxy written in Go. Unlike traditional proxies that rely on static configuration files, Traefik watches the infrastructure it sits in front of -- Docker, Kubernetes, Consul, Nomad, or plain files -- and updates its routing table in real time as services come and go. Add a container with the right labels and Traefik immediately provisions a route, fetches a Let's Encrypt certificate, and starts forwarding traffic. Remove the container and the route disappears.
Version 3, released in 2024, brought several significant upgrades: native HTTP/3 (QUIC) support, a redesigned plugin system, Kubernetes Gateway API v1 compatibility, WebAssembly middleware, and improved observability through OpenTelemetry. The core promise remains unchanged: zero-restart configuration, automatic TLS, and first-class support for modern cloud-native platforms.
Traefik's routing model is built around four concepts. EntryPoints are the listening ports (:80, :443, :8080). Routers match incoming requests by host, path, headers, or query parameters and dispatch them to services (the backends). Middlewares sit between a router and its service and can rewrite requests, add authentication, enforce rate limits, compress responses, or redirect schemes. Everything is composable: one request can pass through a chain of middlewares before it reaches the upstream.
The use cases are broad. Self-hosters run Traefik in front of their *arr media stack, Nextcloud, and Vaultwarden. Small SaaS companies use it as the single ingress for their microservices, with automatic wildcard certificates for customer subdomains. Kubernetes operators deploy it as the IngressRoute controller for entire clusters. And development teams use it in local Docker Compose setups to get real HTTPS URLs during development.
Traefik vs Nginx: When to Choose Which
Both Traefik and Nginx are production-grade reverse proxies, but they occupy different niches.
| Aspect | Traefik v3 | Nginx |
|---|---|---|
| Configuration style | Dynamic (labels, CRDs, files) | Static .conf files |
| Config reloads | Zero-downtime, automatic | Manual nginx -s reload |
| Service discovery | Docker, Kubernetes, Consul, etc. | None (manual) |
| Let's Encrypt | Built-in ACME client | External (certbot, acme.sh) |
| Default routing model | Host/path/label-based | Server blocks |
| Dashboard | Built-in, real-time | None (Nginx Plus only) |
| Raw HTTP performance | Slightly lower at 100k+ rps | Industry-leading |
| Caching | Basic (plugin ecosystem) | Mature, tunable proxy_cache |
| HTTP/3 (QUIC) | Native in v3 | Requires build-time flag |
| Ideal fit | Container/Kubernetes environments | Static config, caching CDNs, high-throughput edge |
Choose Nginx when you need fine-grained caching (proxy_cache_path), you run bare-metal services without a container runtime, you want absolute maximum raw throughput, or you already have a team fluent in Nginx syntax. Our Nginx install guide covers that setup in detail.
For most self-hosted Docker stacks in 2026, Traefik is the more ergonomic choice -- which is why this guide exists.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- A registered domain name with DNS pointing to the server's public IP (required for Let's Encrypt)
- Ports 80 and 443 open in your firewall and your provider's security groups
- At least 1 GB of RAM (Traefik itself uses ~50-100 MB; the rest is for your services)
- A Cloudflare account (only if you want DNS-01 wildcard certificates -- optional)
Recommended Plan: CloudCore Starter>
For a reverse proxy fronting a handful of self-hosted services, the CloudCore Starter plan is more than enough:>
- 4 vCPU cores
- 8 GB RAM
- 200 GB NVMe SSD
- Unmetered bandwidth>
Traefik is lightweight -- the Starter plan leaves plenty of headroom for your backend containers, databases, and a monitoring stack.
Connect to your server via SSH to get started:
ssh root@your-server-ipStep 1: Prepare the Server
Update the package index and install the tools you will need throughout the guide:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget ca-certificates gnupg lsb-release apache2-utils ufwThe apache2-utils package provides htpasswd, which we use later to generate basic-auth credentials.
Open the required firewall ports. Port 22 is SSH; 80 and 443 are HTTP and HTTPS for Traefik.
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp # HTTP/3 QUIC
sudo ufw --force enable
sudo ufw statusExpected output:
Status: active
To Action From -- ------ ---- 22/tcp ALLOW Anywhere 80/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere 443/udp ALLOW Anywhere
Point a DNS A record for at least one subdomain at your server. For the rest of this guide we assume:
traefik.example.com-- the Traefik dashboardwhoami.example.com-- a sample backendexample.comand*.example.com-- for the wildcard cert test
example.com with your real domain throughout.Step 2: Install Docker and Docker Compose
Install Docker Engine from the official Docker apt repository. The version shipped in Ubuntu's default repos is usually several releases behind.
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.ascAdd the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullInstall Docker Engine, CLI, and Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify
docker --version
docker compose versionExpected output:
Docker version 27.5.1, build 9f9e405
Docker Compose version v2.32.4Enable the service so Docker starts on boot:
sudo systemctl enable --now dockerIf you want to run docker commands without sudo, add your user to the docker group (log out and back in afterwards):
sudo usermod -aG docker $USERFor a deeper walkthrough of Docker installation and tuning, see our dedicated Docker Compose guide.
Step 3: Create the Directory Structure
A clean directory layout makes Traefik configuration easier to reason about and back up. Create the following tree under /opt/traefik:
sudo mkdir -p /opt/traefik/{config,config/dynamic,logs,letsencrypt}
sudo touch /opt/traefik/letsencrypt/acme.json
sudo chmod 600 /opt/traefik/letsencrypt/acme.json
cd /opt/traefikThe acme.json file is where Traefik persists its Let's Encrypt account key and issued certificates. Traefik refuses to start if its permissions are wider than 600.
Create a dedicated Docker network that all proxied services will join. Keeping Traefik and its backends on a named external network decouples their compose files.
docker network create traefik_proxyStep 4: Write the Static Configuration (traefik.yml)
Traefik separates static config (loaded at startup: entrypoints, providers, certificate resolvers, logging) from dynamic config (routers, services, middlewares -- can change at runtime). The static file lives at /opt/traefik/config/traefik.yml.
sudo nano /opt/traefik/config/traefik.ymlPaste the following, replacing [email protected] with the email address Let's Encrypt should use for expiry notices:
# /opt/traefik/config/traefik.ymlglobal:
checkNewVersion: true
sendAnonymousUsage: false
----- API & Dashboard -----
api:
dashboard: true
insecure: false # never expose on :8080 in production
debug: false----- EntryPoints -----
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
http3: {} # enable HTTP/3 / QUIC
metrics:
address: ":8082" # internal-only, for Prometheus scraping
----- Providers -----
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # require explicit traefik.enable=true
network: traefik_proxy
watch: true file:
directory: /etc/traefik/dynamic
watch: true
----- Certificate Resolvers -----
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /letsencrypt/acme.json
keyType: EC256
httpChallenge:
entryPoint: web letsencrypt-dns:
acme:
email: [email protected]
storage: /letsencrypt/acme.json
keyType: EC256
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
----- Logs -----
log:
level: INFO # DEBUG for first-time setup, then INFO
filePath: /logs/traefik.log
format: jsonaccessLog:
filePath: /logs/access.log
format: json
bufferingSize: 100
filters:
statusCodes:
- "400-599" # log only errors to keep the file slim
fields:
headers:
defaultMode: drop
names:
User-Agent: keep
Authorization: drop
Cookie: drop
----- Metrics -----
metrics:
prometheus:
entryPoint: metrics
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
addEntryPointsLabels: true
addServicesLabels: trueKey points:
exposedByDefault: falsemeans containers must explicitly opt in withtraefik.enable=true. This prevents accidentally publishing internal services.- Two ACME resolvers are declared:
letsencrypt(HTTP-01, good for most single hostnames) andletsencrypt-dns(DNS-01 via Cloudflare, required for wildcards). keyType: EC256issues ECDSA certificates, which are smaller and faster than RSA and widely supported by modern browsers.- The metrics entryPoint is bound to port
8082, which we will not expose on the host. It is reachable only from inside the Docker network for Prometheus scraping.
Step 5: Write the Dynamic Configuration (dynamic.yml)
Dynamic configuration holds middlewares and TLS options that we want available to any router. Create the file:
sudo nano /opt/traefik/config/dynamic/dynamic.ymlPaste:
# /opt/traefik/config/dynamic/dynamic.ymlhttp: middlewares:
# Force HTTPS (belt-and-braces; the entryPoint redirect already does this) redirect-to-https: redirectScheme: scheme: https permanent: true
# Generic security headers secure-headers: headers: frameDeny: true browserXssFilter: true contentTypeNosniff: true referrerPolicy: "strict-origin-when-cross-origin" stsIncludeSubdomains: true stsPreload: true stsSeconds: 63072000 customResponseHeaders: X-Robots-Tag: "noindex, nofollow, nosnippet, noarchive" customFrameOptionsValue: "SAMEORIGIN" contentSecurityPolicy: "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'"
# Rate limit - 100 average, 200 burst, per client IP rate-limit: rateLimit: average: 100 burst: 200 period: 1s sourceCriterion: ipStrategy: depth: 1 # trust one layer of X-Forwarded-For (Cloudflare)
# Tight rate limit for login endpoints rate-limit-auth: rateLimit: average: 5 burst: 10 period: 1m
# Basic auth for the dashboard. Replace hash in Step 7 labels, not here. dashboard-auth: basicAuth: users: - "admin:$apr1$REPLACE$ME" # replace with real hash realm: "Traefik Dashboard" removeHeader: true
# Forward-auth -> Authelia (see Step 10) authelia: forwardAuth: address: "http://authelia:9091/api/authz/forward-auth" trustForwardHeader: true authResponseHeaders: - "Remote-User" - "Remote-Groups" - "Remote-Email" - "Remote-Name"
# Strip a path prefix before forwarding (useful for path-based routing) strip-api-prefix: stripPrefix: prefixes: - "/api"
# Compress responses compress-gzip: compress: {}
----- TLS Options -----
tls: options: modern: minVersion: VersionTLS13 sniStrict: true
intermediate: minVersion: VersionTLS12 cipherSuites: - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 curvePreferences: - CurveP521 - CurveP384 sniStrict: false
Generate the basic-auth hash for the dashboard. The username below is admin; use any password you like:
htpasswd -nbB admin 'YourStrongPasswordHere'Expected output (the $2y$... hash is bcrypt):
admin:$2y$05$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345We will put the hash into the docker-compose labels in Step 7 (not directly into dynamic.yml) so the username+password travels with the Traefik container definition. When placing it in compose labels, escape each $ as $$ because Docker Compose treats $ as a variable prefix.
Step 6: Configure Cloudflare DNS-01 for Wildcard Certificates
If your domain is on Cloudflare, DNS-01 lets you issue wildcard certificates (*.example.com) and works even if port 80 is blocked. Skip this section if you only need single-hostname certs.
Create an env file that Traefik will read at startup:
sudo nano /opt/traefik/.envPaste:
CF_DNS_API_TOKEN=your-cloudflare-api-token-heresudo chmod 600 /opt/traefik/.envThe compose file in the next step references this via env_file: so the token never appears on the command line or in docker inspect output.
Step 7: Launch Traefik with docker-compose
Create the compose file:
sudo nano /opt/traefik/docker-compose.ymlPaste:
# /opt/traefik/docker-compose.ymlservices: traefik: image: traefik:v3.3 container_name: traefik restart: unless-stopped security_opt: - no-new-privileges:true ports: - "80:80" - "443:443" - "443:443/udp" # HTTP/3 env_file: - .env volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./config/traefik.yml:/etc/traefik/traefik.yml:ro - ./config/dynamic:/etc/traefik/dynamic:ro - ./letsencrypt:/letsencrypt - ./logs:/logs networks: - traefik_proxy labels: - "traefik.enable=true"
# ----- Dashboard router ----- - "traefik.http.routers.dashboard.rule=Host(
traefik.example.com)" - "traefik.http.routers.dashboard.entrypoints=websecure" - "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.tls=true" - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt" - "traefik.http.routers.dashboard.tls.options=modern@file" - "traefik.http.routers.dashboard.middlewares=dashboard-auth-inline,secure-headers@file,rate-limit@file"# ----- Inline basic-auth middleware (replace with your bcrypt hash) ----- # IMPORTANT: Each '$' in the hash MUST be escaped as '$$' in compose labels - "traefik.http.middlewares.dashboard-auth-inline.basicauth.users=admin:$$2y$$05$$REPLACE_WITH_YOUR_HASH"
# ----- Wildcard certificate (optional, requires Cloudflare DNS-01) ----- - "traefik.http.routers.dashboard.tls.domains[0].main=example.com" - "traefik.http.routers.dashboard.tls.domains[0].sans=*.example.com" # To use DNS-01 for the wildcard instead of HTTP-01, swap the certresolver above: # - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt-dns"
networks: traefik_proxy: external: true
Replace traefik.example.com and example.com with your domain, and paste your bcrypt hash (with $ -> $$) into the dashboard-auth-inline label.
Bring Traefik up:
cd /opt/traefik
docker compose up -dExpected output:
[+] Running 2/2
✔ Network traefik_proxy External
✔ Container traefik StartedWatch the logs to confirm the ACME flow:
docker compose logs -f traefikYou should see:
level=info msg="Configuration loaded from file: /etc/traefik/traefik.yml"
level=info msg="Starting provider *docker.Provider"
level=info msg="Starting provider *file.Provider"
level=info msg="Starting provider *acme.Provider"
level=info msg="Testing certificate renew..."The first certificate issuance takes 10-30 seconds. Press Ctrl+C to exit the log stream.
Step 8: Verify the Installation
Visit https://traefik.example.com in your browser. You should get a basic-auth prompt. Enter the credentials you set in Step 5 and you will see the Traefik dashboard with tabs for HTTP, TCP, UDP, and the certificate store.
Verify the certificate from the command line:
curl -vI https://traefik.example.com 2>&1 | grep -E "subject:|issuer:|expire"Expected output:
* Server certificate:
- subject: CN=traefik.example.com
- start date: Apr 16 10:00:00 2026 GMT
- expire date: Jul 15 10:00:00 2026 GMT
issuer: C=US; O=Let's Encrypt; CN=E5
curl -sI https://traefik.example.com | grep -i alt-svcExpected: a header like alt-svc: h3=":443"; ma=2592000.
Check that port 80 correctly redirects to 443:
curl -sI http://traefik.example.com | head -1Expected: HTTP/1.1 308 Permanent Redirect.
Step 9: Route Your First Service with Docker Labels
Let's put a real backend behind Traefik. The traefik/whoami image is a minimal HTTP server that echoes request details -- perfect for testing routing.
Create a new compose file in its own directory:
sudo mkdir -p /opt/whoami
sudo nano /opt/whoami/docker-compose.ymlPaste:
services: whoami: image: traefik/whoami:latest container_name: whoami restart: unless-stopped networks: - traefik_proxy labels: - "traefik.enable=true" - "traefik.docker.network=traefik_proxy"# HTTPS router - "traefik.http.routers.whoami.rule=Host(
whoami.example.com)" - "traefik.http.routers.whoami.entrypoints=websecure" - "traefik.http.routers.whoami.tls=true" - "traefik.http.routers.whoami.tls.certresolver=letsencrypt" - "traefik.http.routers.whoami.middlewares=secure-headers@file,rate-limit@file,compress-gzip@file"# Backend port inside the container - "traefik.http.services.whoami.loadbalancer.server.port=80"
networks: traefik_proxy: external: true
Launch it:
cd /opt/whoami
docker compose up -dWithin seconds Traefik discovers the container, requests a certificate for whoami.example.com, and starts routing:
curl https://whoami.example.comExpected output:
Hostname: 5f2a1b3c4d6e
IP: 127.0.0.1
IP: 172.22.0.3
RemoteAddr: 172.22.0.2:52314
GET / HTTP/1.1
Host: whoami.example.com
User-Agent: curl/8.5.0
Accept: /
X-Forwarded-For: 203.0.113.42
X-Forwarded-Host: whoami.example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: 5e9a2b1c8f7d
X-Real-Ip: 203.0.113.42From here, any new container with the right labels becomes a live site. No reloads, no edits to Traefik's config.
Common Label Patterns
| Goal | Labels | ||||
|---|---|---|---|---|---|
| HTTPS with auto cert | traefik.enable=true, router .rule, .entrypoints=websecure, .tls.certresolver=letsencrypt | ||||
| Multiple domains on one service | <code> rule=Host(</code>a.com) | Host(b.com) `</td></tr><tr><td>Path-based routing</td><td> rule=Host(example.com) && PathPrefix(/api) </td></tr><tr><td>Strip path prefix</td><td>attach middleware <code>strip-api-prefix@file</code></td></tr><tr><td>Redirect www to root</td><td>dedicated router with <code>redirectregex</code> middleware</td></tr><tr><td>Weighted load balancing</td><td><code>traefik.http.services.NAME.loadbalancer.weighted</code> plus multiple backend services</td></tr><tr><td>Sticky sessions</td><td><code>traefik.http.services.NAME.loadbalancer.sticky.cookie=true</code></td></tr><tr><td>Custom health check</td><td><code>traefik.http.services.NAME.loadbalancer.healthcheck.path=/healthz</code></td></tr></tbody></table></div>
| grep -i acme. For Cloudflare DNS-01, confirm token is loaded: docker exec traefik env \ | grep CF_DNS</td></tr><tr><td>Basic-auth prompt loops forever</td><td>Hash contains unescaped <code>$</code> in compose labels</td><td>Replace each <code>$</code> with <code>$$</code> in docker-compose labels</td></tr><tr><td><code>Gateway Timeout</code> when hitting a backend</td><td>Backend not on <code>traefik_proxy</code> network or wrong port label</td><td><code>docker network inspect traefik_proxy</code> and confirm backend is attached; verify <code>traefik.http.services.NAME.loadbalancer.server.port</code> matches the container's listening port</td></tr><tr><td>HTTP/3 not negotiated</td><td>UDP 443 closed on firewall</td><td><code>sudo ufw allow 443/udp</code> and open UDP at the cloud provider firewall layer</td></tr><tr><td>Rate limit blocks real traffic</td><td><code>ipStrategy.depth</code> wrong for your proxy chain</td><td>If you sit directly on the internet, set <code>depth: 0</code>. Behind one Cloudflare layer, <code>depth: 1</code>. Behind Cloudflare and a load balancer, <code>depth: 2</code></td></tr><tr><td><code>permission denied</code> reading acme.json</td><td>File permissions too open</td><td>Must be exactly <code>600</code>: <code>chmod 600 /opt/traefik/letsencrypt/acme.json</code></td></tr><tr><td>Dashboard works but shows no routers</td><td>Containers not on the same network, or <code>exposedByDefault=false</code> plus missing <code>traefik.enable=true</code> label</td><td>Add the label, restart the backend container</td></tr></tbody></table></div>
|