How to Deploy Traefik on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Run Traefik as the front door of a Docker VPS — label-driven routing, automatic Let's Encrypt certificates, an HTTP-to-HTTPS redirect, and a dashboard that is not open to the internet.
- A VPS of any size — Traefik itself needs well under 256 MB RAM
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain whose A records you can point at the server
- Docker Engine + Compose installed (see the base guide below)
- Comfort reading Docker labels — they are Traefik's whole configuration surface
What Traefik is
Traefik is a reverse proxy written in Go under the MIT licence. It sits on ports 80 and 443, receives every request, and forwards each one to the right container. What sets it apart from Caddy or a hand-written nginx config is where the routing comes from: Traefik watches the Docker socket and builds its routes from labels on your containers. You start a new app with the right three labels and it is live on its own hostname, with a certificate, without editing a proxy config or reloading anything.
That design pays off once you run several apps on one box. For one or two apps it is more ceremony than Caddy, and the trade-off is laid out in Caddy vs Traefik. If you would rather click through a web UI than write labels, Nginx Proxy Manager is the other common choice; Nginx Proxy Manager vs Traefik compares the two. We rate Traefik 3 / 5 to deploy: the install is short, but the label vocabulary takes an afternoon to learn.
Server sizing
Traefik is a single Go binary with a small footprint. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1, September 2026) an idle Traefik container used about 14 MB of RAM and about 242 MB of disk for the image. Our catalog lists 256 MB as the working minimum.
In practice the proxy is never what sizes the server — the apps behind it are. Pick the VPS for your workload and treat Traefik as free. The one resource it does care about is file descriptors and connections under heavy traffic, which is not a concern at self-hosting scale.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first.
Open SSH and the two web ports:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
One caveat that matters for every proxy on a Docker host: ports published
with Docker's ports: bypass ufw, because Docker writes its own iptables
rules. A container published as 8080:8080 is reachable from the internet
whatever ufw says. The fix is not to publish app ports at all — the apps sit
on a private Docker network and only Traefik publishes 80 and 443. The compose
file below is built that way.
Paid link — we earn a commission if you shop through it.
Install Traefik (Docker Compose)
Create a working directory and a shared Docker network. Every app you want
Traefik to route to joins this proxy network:
mkdir -p ~/traefik/letsencrypt && cd ~/traefik
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy
The dashboard is useful and also a map of your whole setup, so it goes behind basic auth from the start. Generate a password and its hash once, keeping the plain password in a file only you can read:
cd ~/traefik
if [ ! -f .env ]; then
PASS="$(openssl rand -hex 16)"
echo "$PASS" > dashboard-password.txt && chmod 600 dashboard-password.txt
printf "DASHBOARD_AUTH='admin:%s'\n" "$(openssl passwd -apr1 "$PASS")" > .env
fi
The single quotes in .env matter: the hash contains $ characters, and
Compose leaves single-quoted values alone instead of trying to expand them.
Now the compose file. It runs Traefik plus traefik/whoami, a tiny test app
from the Traefik project that echoes the request back, so you can prove the
routing works before you put anything real behind it. Replace the email
address and the two example.com hostnames with your own:
cd ~/traefik
cat > docker-compose.yml <<'YAML'
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- proxy
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--entryPoints.web.address=:80"
- "--entryPoints.websecure.address=:443"
- "--entryPoints.web.http.redirections.entryPoint.to=websecure"
- "--entryPoints.web.http.redirections.entryPoint.scheme=https"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
# While testing, uncomment to use Let's Encrypt staging (no rate limits, untrusted certs):
# - "--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
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=le"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=${DASHBOARD_AUTH}"
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- 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=le"
networks:
proxy:
external: true
YAML
Start it:
cd ~/traefik
docker compose up -d
docker compose ps
What each part does:
exposedbydefault=falsemeans Traefik ignores every container that does not carrytraefik.enable=true. Leave this on. The default istrue, which publishes every container on the host the moment it starts, including databases you never meant to expose.providers.docker.network=proxytells Traefik which network to use when it connects to a container. Without it, a container on two networks can be reached on the wrong one, and you get gateway timeouts.- The two
redirectionsflags send every plain-HTTP request to HTTPS. The ACME HTTP challenge still works, because Traefik answers it before the redirect applies. certresolver=leon a router is what asks Let's Encrypt for a certificate for that router'sHost(). The HTTP-01 challenge needs thewebentrypoint reachable from the internet on port 80.service=api@internalis Traefik's built-in dashboard service. The dashboard is only reachable through that router, so it gets HTTPS and the basic-auth middleware like any app.
Check the routing before DNS
You can prove the routing works without any DNS at all, by telling curl
which IP to use for a hostname. Until a real certificate is issued, Traefik
serves its own self-signed default certificate, hence -k:
sleep 5
curl -sk --resolve whoami.example.com:443:127.0.0.1 https://whoami.example.com/ | head -5
curl -s -o /dev/null -w "HTTP redirect: %{http_code}\n" -H "Host: whoami.example.com" http://127.0.0.1/
curl -sk -o /dev/null -w "dashboard without password: %{http_code}\n" \
--resolve traefik.example.com:443:127.0.0.1 https://traefik.example.com/dashboard/
curl -sk -o /dev/null -w "dashboard with password: %{http_code}\n" \
-u "admin:$(cat ~/traefik/dashboard-password.txt)" \
--resolve traefik.example.com:443:127.0.0.1 https://traefik.example.com/dashboard/
You should see the whoami echo (Hostname: …, GET / HTTP/1.1), a 301
redirect from port 80, a 401 for the dashboard without credentials and a
200 with them. If the whoami line is 404 page not found, no router matched
the hostname: check the Host() label.
HTTPS + domain
Now point real DNS at the box. Create A records for each hostname in your
labels (whoami.example.com, traefik.example.com, and later one per app)
pointing at the server's public IP. A wildcard record *.example.com saves you
from adding one per app.
Once a name resolves, Traefik requests its certificate on the next request (and retries on its own). Watch it happen:
docker compose logs -f traefik
curl -sI https://whoami.example.com/
Two habits save you from Let's Encrypt rate limits: test new setups with the
staging caserver line uncommented, and only switch to production once
staging issues cleanly. When you switch, delete letsencrypt/acme.json so the
staging certificates are not kept.
letsencrypt/acme.json holds the private keys for every certificate
Traefik has issued. Treat it like a password file: back it up, and never
commit it anywhere.
Put a real app behind it
Any compose project joins by attaching to the proxy network and adding
labels. For an app listening on port 3000 inside its container:
services:
myapp:
image: example/myapp:latest
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=le"
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
networks:
proxy:
external: true
No ports: section: the app is reachable only through Traefik. If the
image exposes exactly one port Traefik uses it on its own; if it exposes several
Traefik picks the lowest, and if it exposes none it cannot guess — so setting
loadbalancer.server.port explicitly is the safe habit.
Router names (myapp) must be unique across the whole host — two projects that
both define a router called web will fight.
Once whoami has done its job, remove its service block and run
docker compose up -d --remove-orphans.
Securing it
- The Docker socket is the real risk. Traefik's own docs carry a security
note: unrestricted access to the Docker API means that if Traefik is
compromised, the attacker may reach the host. The
:roflag on the mount does not limit which API calls can be made. On a shared or high-value box, put a socket proxy such as Tecnativa'sdocker-socket-proxybetween Traefik and Docker so it can only read container information. - Never use
--api.insecure=trueon a public server. It serves the dashboard and API without authentication on port 8080. The router above (HTTPS plus basic auth) is the safe way to expose it, and you can drop the dashboard router entirely if you do not need it. - Keep
exposedbydefault=falseand addtraefik.enable=trueonly where you mean it. no-new-privilegesstops processes in the Traefik container from gaining privileges; it costs nothing.
Backups
Traefik holds almost no state. What you need to keep is the configuration and the certificate store:
cd ~/traefik
tar czf ~/traefik-backup-$(date +%F).tar.gz docker-compose.yml .env letsencrypt
ls -lh ~/traefik-backup-*.tar.gz
Copy the archive off the box. Losing acme.json is not fatal — Traefik
requests new certificates on the next start — but if you run many domains a
mass re-issue can hit Let's Encrypt rate limits, so keeping it is worth the
few kilobytes.
Upgrades
cd ~/traefik
docker compose pull
docker compose up -d
The traefik:v3 tag follows the v3 line, so a pull brings in minor and patch
releases but never a major version. Read the migration guide before moving
to a future v4. Moving from v2 to v3 changed some rule syntax and options, so
old v2 labels copied from blog posts may not work as-is.
Troubleshooting
404 page not found for a hostname. No router matched. Check the
Host() rule for typos, that the container has traefik.enable=true, and
that it is running. The dashboard's HTTP → Routers page lists every router
Traefik knows about, with errors shown next to broken ones.
Gateway Timeout or Bad Gateway. Traefik found the router but cannot
reach the container: it is on a different network, or Traefik picked the wrong
port. Put it on proxy and set
traefik.http.services.<name>.loadbalancer.server.port.
The browser shows a certificate warning. Traefik is still serving its
self-signed default certificate because it has not obtained a real one. Check
docker compose logs traefik for ACME errors: usually DNS not pointing here
yet, port 80 blocked by a cloud firewall, or a rate limit.
The dashboard asks for a password and rejects it. The hash in .env was
mangled. Make sure it is wrapped in single quotes, then recreate the
container with docker compose up -d --force-recreate traefik.
Verification + next steps
You're done when: https://whoami.example.com loads over a valid Let's Encrypt
certificate, http:// redirects to it, the dashboard asks for credentials and
lists your routers, and no app on the box publishes its own port.
From there, put your apps behind it one compose file at a time. Traefik is also what Coolify and Dokploy run under the hood, so if you later want a full deploy platform rather than a bare proxy, the concepts carry over. For hosting picks, see Best VPS for Docker.