Skip to content

How to Deploy Traefik on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Run 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.

Before you start
  • 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
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

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.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

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=false means Traefik ignores every container that does not carry traefik.enable=true. Leave this on. The default is true, which publishes every container on the host the moment it starts, including databases you never meant to expose.
  • providers.docker.network=proxy tells 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 redirections flags send every plain-HTTP request to HTTPS. The ACME HTTP challenge still works, because Traefik answers it before the redirect applies.
  • certresolver=le on a router is what asks Let's Encrypt for a certificate for that router's Host(). The HTTP-01 challenge needs the web entrypoint reachable from the internet on port 80.
  • service=api@internal is 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 :ro flag 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's docker-socket-proxy between Traefik and Docker so it can only read container information.
  • Never use --api.insecure=true on 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=false and add traefik.enable=true only where you mean it.
  • no-new-privileges stops 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.

Next steps

How to self-host Traefik →More self-hosted reverse proxy tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.