Bitdoze Logo

Traefik Reverse Proxy in Docker: Complete Setup Guide

Set up Traefik as a Docker reverse proxy with automatic Let's Encrypt TLS certificates. Step-by-step guide covering v3.7, dashboard security, and app deployment.

DragosDragos23 min read
Traefik Reverse Proxy in Docker: Complete Setup Guide

If you’re running Docker on a VPS and need a reverse proxy that handles TLS certificates automatically, Traefik is the best option. I’ve been using Cloudflare Tunnels for most of my setups, but Traefik gives you full control. No third-party tunnel dependency, automatic Let’s Encrypt certificates, and native Docker integration. In this guide, we’ll set up Traefik as a reverse proxy in Docker from scratch: VPS creation, Docker install, Traefik v3.7 configuration, dashboard security, and your first app behind the proxy.

Security Warning

The original version of this guide used Traefik v3.1, which reached end of life on October 28, 2024 and has known critical vulnerabilities (CVE-2024-45410, CVSS 7.5 HIGH per NVD / 9.8 per GitHub’s advisory). This guide has been updated to Traefik v3.7. If you are running v3.1, upgrade immediately.

If you need a Let’s Encrypt wildcard certificate with Cloudflare DNS challenge, see: Traefik FREE Let’s Encrypt Wildcard Certificate With Cloudflare Provider

What is Traefik?

Traefik is a modern reverse proxy and load balancer designed for containerized environments. It routes traffic to your microservices and applications by automatically discovering and configuring routes based on your infrastructure.

Traefik’s Docker integration is what sets it apart. It detects new containers and updates its routing config automatically. You don’t have to touch a config file every time you add a service.

Main features:

  1. Automatic service discovery and configuration
  2. Support for multiple protocols (HTTP, HTTPS, TCP, UDP)
  3. Built-in monitoring dashboard
  4. Built-in Let’s Encrypt integration for automatic SSL/TLS certificate management
  5. Support for various load balancing algorithms
  6. Middleware for adding extra functionality like authentication or rate limiting
Traefik architecture diagram showing EntryPoints, Routers, and Middlewares flow

Traefik’s architecture

Traefik’s architecture is built around three main components: EntryPoints, Routers, and Middlewares.

EntryPoints

EntryPoints are the network entry points into Traefik. They define the ports and protocols on which Traefik listens for incoming traffic.

What EntryPoints do:

  • Define listening ports for HTTP, HTTPS, or UDP traffic
  • Can be configured for TCP and UDP protocols
  • Support for multiple EntryPoints (e.g., separate ones for HTTP and HTTPS)
  • Can be associated with specific IP addresses

Routers

Routers are responsible for connecting incoming requests to the services that can handle them. They analyze the requests using rules and route them accordingly.

What Routers do:

  • Use rules to determine which requests they should handle
  • Can be associated with specific EntryPoints
  • Support priority settings to manage overlapping rules
  • Can be configured for HTTP, TCP, or UDP traffic

Middlewares

Middlewares tweak the requests before they are sent to your service (or the responses before they are sent back to the clients). They can be attached to routers and provide a way to apply modifications to requests or responses.

What Middlewares do:

  • Can modify requests and responses
  • Chainable (multiple middlewares can be applied in sequence)
  • Provide functionality such as authentication, rate limiting, headers manipulation, etc.
  • Can be reused across multiple routers

Traefik has three core components: EntryPoints (where it listens), Routers (how it handles requests), and Middlewares (request/response modifications). Together they form a flexible routing system for containerized environments.

How to setup Traefik as a reverse proxy for your Docker apps

Want to monitor server resources like CPU, memory, and disk space? See: How To Monitor Server and Docker Resources

After we have seen what Traefik is, we are going to go through all the steps needed: create and configure a VPS, install Docker, configure DNS, set up Traefik with the dashboard, and deploy some applications.

1. Create a VPS server

You need a VPS with ports 22, 80, and 443 open. I use Hetzner or Hostinger for most setups. Use your provider’s cloud firewall if available (e.g., Hetzner Cloud Firewall). It’s more reliable than UFW alone because of how Docker interacts with iptables (more on that in Step 4).

For hardening your VPS beyond basic firewall rules, consider securing your VPS with CrowdSec. It works well alongside Traefik.

2. Add SWAP

Most VPS servers don’t have swap by default. Add it with:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

3. Install Docker

The next step is installing Docker and Docker Compose v2. The commands below auto-detect your distro codename (works for both Ubuntu and Debian):

# Add Docker's official GPG key
sudo apt-get update
sudo apt-get install ca-certificates curl
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.asc

# Add the repository (auto-detects Ubuntu/Debian codename)
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/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify the install worked:

docker --version
docker compose version

You should see version output for both. If docker compose version errors, the Compose plugin wasn’t installed correctly.

4. Configure firewall and update OS

Update the OS first:

sudo apt update && sudo apt upgrade -y

If you’re using UFW, open the required ports:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status

Docker bypasses UFW

Docker manipulates iptables directly, which can bypass UFW rules. Containers with published ports may be accessible even if UFW blocks that port. Consider using your cloud provider’s firewall (e.g., Hetzner Cloud Firewall) or the DOCKER-USER iptables chain. See: Docker Bypasses UFW Firewall Rules

Reboot after updates:

reboot

5. Create the Docker network

Create the external network that Traefik and your apps will share:

docker network create traefik-net

Verify:

docker network ls | grep traefik-net

You should see traefik-net listed with bridge driver.

6. Create the Traefik reverse proxy Docker Compose file

Create the directory and navigate to it:

mkdir -p /opt/stacks/traefik && cd /opt/stacks/traefik

This guide uses the TLS-ALPN-01 challenge, which requires port 443 to be reachable from the internet. If you need wildcard certificates or can’t open port 443, use DNS challenge instead. See: Traefik Let’s Encrypt Wildcard Certificate

The recommended setup uses a Docker socket proxy to limit Traefik’s access to the Docker API. If you want the simpler direct-socket version, use the second tab below.

Command options explained:

  1. --api.dashboard=true: Enables the Traefik web dashboard.
  2. --providers.docker=true: Enables Docker as a provider for automatic service discovery.
  3. --providers.docker.exposedbydefault=false: Prevents Traefik from automatically exposing all containers. You must explicitly enable each one.
  4. --providers.docker.endpoint=tcp://socket-proxy:2375: Connects to the Docker API through the socket proxy instead of the raw socket. (Omitted in the direct-socket version.)
  5. --providers.docker.network=traefik-net: Tells Traefik which Docker network to use for routing traffic to containers.
  6. --entrypoints.http.address=:80: HTTP entrypoint on port 80.
  7. --entrypoints.http.http.redirections.entrypoint.to=https: Redirect all HTTP traffic to HTTPS.
  8. --entrypoints.http.http.redirections.entrypoint.scheme=https: Ensures the redirect uses the HTTPS scheme. For a deeper dive on HTTP to HTTPS redirects, see Traefik HTTP to HTTPS redirect.
  9. --entrypoints.https.address=:443: HTTPS entrypoint on port 443.
  10. --certificatesresolvers.letsencrypt.acme.tlschallenge=true: Enables the TLS-ALPN-01 challenge for Let’s Encrypt certificate acquisition.
  11. [email protected]: Your email for Let’s Encrypt registration and expiry notifications.
  12. --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json: Where to store certificates. This file needs specific permissions (covered in Step 9).
  13. --ping=true: Enables the healthcheck endpoint used by Docker healthcheck.

Dashboard labels explained:

  1. traefik.enable=true: Enables Traefik for this container.
  2. traefik.http.routers.traefik-secure.rule=Host(traefik.yourdomain.com): Routes requests for this hostname to the dashboard.
  3. traefik.http.routers.traefik-secure.entrypoints=https: Uses the HTTPS entrypoint.
  4. traefik.http.routers.traefik-secure.service=api@internal: Routes to Traefik’s internal dashboard API.
  5. traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt: Uses Let’s Encrypt for TLS certificates.
  6. traefik.http.routers.traefik-secure.middlewares=traefik-auth: Applies the auth middleware.
  7. traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}: Sets up Basic Auth. Credentials come from the .env file. More on Traefik Basic Authentication.
  8. traefik.http.routers.traefik-secure.tls=true: Enables TLS for this router.

Replace traefik.yourdomain.com with your actual domain.

7. Create the .env file for Traefik dashboard credentials

Install htpasswd:

sudo apt install apache2-utils -y

Generate a bcrypt hash and write it to .env with the variable name:

echo "TRAEFIK_DASHBOARD_CREDENTIALS=$(htpasswd -nbB admin 'YourPassword123' | sed 's/\$/$$/g')" | sudo tee -a .env

The -B flag uses bcrypt. The sed 's/\$/$$/g' doubles every $: Docker Compose interpolates $VAR inside .env files, so a single-$ hash would get mangled (the letters after $ are eaten as an undefined variable) and auth would silently fail with 401. With $$ the container receives the correct single-$ hash.

Verify the .env contains the doubled hash:

cat .env
# TRAEFIK_DASHBOARD_CREDENTIALS=admin:$$2y$$05$$...hash...

Replace with the actual output from the htpasswd command.

For production, consider using Docker Compose secrets instead of .env files for sensitive credentials.

8. Point the domain to your server IP

Create an A record for your domain pointing to the server IP. For subdomains, you can either:

  • Create individual A records for each subdomain (e.g., traefik.yourdomain.com, flowise.yourdomain.com)
  • Create a wildcard A record (*.yourdomain.com) pointing to the server

Individual records are more explicit and easier to debug. If you’re not using a wildcard, make sure the traefik.yourdomain.com record is created before proceeding.

Verify DNS propagation:

dig traefik.yourdomain.com +short

Should return your server IP. If it returns nothing, wait a few more minutes. The TLS challenge requires DNS to resolve correctly.

9. Start Traefik and verify Let’s Encrypt TLS certificates

Before starting, create the acme.json file with correct permissions:

mkdir -p ./letsencrypt
touch ./letsencrypt/acme.json
chmod 600 ./letsencrypt/acme.json

acme.json permissions

If acme.json does not have 600 permissions, Traefik will refuse to start or fail to store certificates. This is the #1 beginner issue. Verify with ls -la ./letsencrypt/acme.json: the output should show -rw-------.

Start Traefik:

docker compose up -d

Verify it works:

  1. Check container status: docker ps: the traefik container should show “healthy” (the healthcheck takes ~30 seconds).

  2. Check logs for cert issuance:

docker logs traefik

Look for a line about certificate being obtained. No errors about acme.json or permissions.

  1. Test HTTP→HTTPS redirect:
curl -I http://traefik.yourdomain.com

Expect a 301 Moved Permanently redirecting to HTTPS.

  1. Test TLS certificate:
curl -vI https://traefik.yourdomain.com 2>&1 | grep -i "issuer"

Should show issuer: CN=R3, O=Let's Encrypt, C=US (or similar). If you see a self-signed cert, DNS isn’t propagated or port 443 isn’t reachable.

  1. Access the dashboard: Open https://traefik.yourdomain.com in your browser. You should get a Basic Auth prompt, then see the Traefik dashboard.

During testing, uncomment the staging CA server line in your compose file to avoid hitting Let’s Encrypt rate limits. Switch back to the production server once everything works. For more essential Docker debugging commands, see: essential Docker commands

Let’s Encrypt certificate note: Certificates are valid for 90 days currently. Traefik auto-renews them (starting 30 days before expiry). Let’s Encrypt is transitioning to 45-day certificates by February 2028. Traefik’s auto-renewal will handle this transparently, but acme.json must remain writable and the container must stay running for renewals to succeed.

10. Deploy your first app behind Traefik reverse proxy

Now that Traefik is running, we can add applications. Here’s a FlowiseAI example with a PostgreSQL database. Previously I used FlowiseAI with Docker Compose behind Cloudflare Tunnels. Now we’re using Traefik labels instead:

services:
  flowise-db:
    image: postgres:16-alpine
    hostname: flowise-db
    networks:
      - traefik-net
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./flowise-db-data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

  flowise:
    image: flowiseai/flowise:latest
    container_name: flowiseai
    hostname: flowise
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:${PORT}
    volumes:
      - ./flowiseai:/root/.flowise
    environment:
      DEBUG: false
      PORT: ${PORT}
      FLOWISE_USERNAME: ${FLOWISE_USERNAME}
      FLOWISE_PASSWORD: ${FLOWISE_PASSWORD}
      APIKEY_PATH: /root/.flowise
      SECRETKEY_PATH: /root/.flowise
      LOG_LEVEL: info
      LOG_PATH: /root/.flowise/logs
      DATABASE_TYPE: postgres
      DATABASE_PORT: 5432
      DATABASE_HOST: flowise-db
      DATABASE_NAME: ${POSTGRES_DB}
      DATABASE_USER: ${POSTGRES_USER}
      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}
    restart: on-failure:5
    networks:
      - traefik-net
    depends_on:
      flowise-db:
        condition: service_healthy
    entrypoint: /bin/sh -c "sleep 3; flowise start"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.flowise.rule=Host(`flowise.domain.com`)"
      - "traefik.http.routers.flowise.entrypoints=https"
      - "traefik.http.routers.flowise.tls.certresolver=letsencrypt"
      - "traefik.http.services.flowise.loadbalancer.server.port=${PORT}"

networks:
  traefik-net:
    external: true

The flowiseai/flowise:latest tag always pulls the newest image. For stability, check the Flowise releases and pin to a specific version instead.

A few things to note about this config:

Both flowise-db and flowise are on traefik-net. The DB and Flowise can also share a separate internal network for database traffic. They just both need traefik-net for Traefik to route external traffic.

The Traefik-specific labels tell Traefik to route traffic for flowise.domain.com to this container on the HTTPS entrypoint, using Let’s Encrypt for TLS.

We don’t publish any host ports. Traefik routes traffic through the Docker network internally. The loadbalancer.server.port label tells Traefik which port the app listens on inside the container.

Replace flowise.domain.com with your actual domain.

Create the .env file for Flowise:

PORT=3000
POSTGRES_USER='user'
POSTGRES_PASSWORD='pass'
POSTGRES_DB='flowise'
FLOWISE_USERNAME=bitdoze
FLOWISE_PASSWORD=bitdoze

If the subdomain isn’t using a wildcard, make sure flowise.domain.com A record points to your server IP first. Then:

source .env
docker compose up -d

Verify: docker logs flowiseai and open https://flowise.domain.com in your browser.

For more Docker container ideas to deploy behind Traefik, check out Docker containers for your home server.

11. Install Dockge to manage your Docker Compose files

Dockge is a lightweight Docker Compose manager with a web UI. I’ve written a detailed Dockge Docker Compose manager install guide. Here’s how to run it behind Traefik.

Create a directory:

mkdir /opt/dockge
cd /opt/dockge

Create a compose.yml file:

services:
  dockge:
    image: louislam/dockge:1
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/stacks:/opt/stacks
    environment:
      - DOCKGE_STACKS_DIR=/opt/stacks
    networks:
      - traefik-net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dockge.rule=Host(`dockge.domain.com`)"
      - "traefik.http.routers.dockge.entrypoints=https"
      - "traefik.http.routers.dockge.tls.certresolver=letsencrypt"
      - "traefik.http.services.dockge.loadbalancer.server.port=5001"

networks:
  traefik-net:
    external: true

The louislam/dockge:1 tag follows the major version, which is the correct practice for stability. Dockge 1.5.0+ disables the built-in terminal/console by default for security. If you need terminal access, add DOCKGE_ENABLE_CONSOLE=true to the environment section.

If you’re not using a wildcard for subdomains, make sure dockge.domain.com A record is pointing to your server IP. Then:

docker compose up -d

Verify: docker logs dockge and open https://dockge.domain.com in your browser.

For a broader comparison of self-hosted management tools, see self-hosted server management panels.

Troubleshooting common problems

Traefik won't start / acme.json errors

The most common cause is wrong permissions on acme.json. Traefik requires 600 permissions on this file.

ls -la ./letsencrypt/acme.json
# Should show: -rw------- 1 root root ...

If permissions are wrong:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Also check docker logs traefik for other errors (port conflicts, network issues).

No certificate / self-signed cert warning

This means the TLS challenge failed. Common causes:

  1. DNS not propagated: Verify with dig traefik.yourdomain.com +short: it must return your server IP.
  2. Port 443 not reachable: The TLS-ALPN-01 challenge requires inbound connections on port 443. Check your firewall and cloud provider security groups.
  3. Cloudflare proxy enabled: If using Cloudflare with orange cloud (proxy), the TLS challenge may fail. Try grey cloud (DNS only) during setup.

Use the staging server while debugging to avoid rate limits. Uncomment the caserver line in your compose file.

Too many certificates already issued

Let’s Encrypt rate limits: 5 duplicate certificates per domain per week. If you’ve been debugging, you may have hit this.

Fix: Switch to the staging server while testing:

- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory

Staging certs show browser warnings but let you verify the setup works. Switch back to production once everything is confirmed.

Docker containers not discovered by Traefik

Check two things:

  1. The container is on the traefik-net network:
docker network inspect traefik-net

Your container should appear in the output.

  1. The container has the traefik.enable=true label:
docker inspect your-container | grep -i traefik.enable
Dashboard shows 401 / auth not working

The most common cause is a single-$ hash in .env getting mangled by Docker Compose interpolation. Regenerate with the $$ escaping from Step 7:

echo "TRAEFIK_DASHBOARD_CREDENTIALS=$(htpasswd -nbB admin 'YourPassword123' | sed 's/\$/$$/g')" | sudo tee .env

Make sure the .env is in the same directory as the compose file and that the env_file: .env directive is present. Also verify the middleware label references traefik-auth correctly.

HTTP redirect loop

If you’re behind Cloudflare, the most common cause is the SSL/TLS mode set to “Flexible” instead of “Full” (or “Full (strict)”). Cloudflare “Flexible” connects to your origin over HTTP, Traefik redirects to HTTPS, Cloudflare connects over HTTP again, creating an infinite loop.

Fix: In Cloudflare dashboard, go to SSL/TLS and set it to Full (or Full (strict) if you have a valid cert).

Use the Let’s Encrypt staging server while testing to avoid hitting rate limits. Uncomment the caserver line in your compose file, and switch back to production once everything works.

Hardening and production notes

Once your basic setup works, here’s what to do for production:

  • Socket proxy (already in recommended setup). It limits Traefik’s Docker API access to read-only container/network/service info
  • Security headers middleware: add HSTS, content-type sniffing protection, XSS filter
  • Rate limiting middleware: protect the dashboard and apps from abuse
  • Backup acme.json: losing it means re-issuing all certificates (and hitting rate limits). Back it up periodically or use a Docker volume to persistent storage
  • Use Docker secrets for credentials instead of .env files in production
  • Keep Traefik updated. traefik:v3.7 tracks the v3.7.x patch releases. Check for new minor versions periodically

Security headers middleware: add these labels to your app containers:

- traefik.http.middlewares.sec-headers.headers.sslredirect=true
- traefik.http.middlewares.sec-headers.headers.stsseconds=63072000
- traefik.http.middlewares.sec-headers.headers.stsincludeSubdomains=true
- traefik.http.middlewares.sec-headers.headers.stspreload=true
- traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.sec-headers.headers.browserXssFilter=true

Rate limiting middleware: add to your router:

- traefik.http.middlewares.rate-limit.ratelimit.average=100
- traefik.http.middlewares.rate-limit.ratelimit.burst=50
- traefik.http.routers.traefik-secure.middlewares=traefik-auth,rate-limit

Explore community plugins at plugins.traefik.io for CrowdSec integration, geo filtering, and more. For broader VPS hardening, see: secure your VPS with CrowdSec

Conclusions

Setting up Traefik as a docker reverse proxy for your self-hosted apps is straightforward once you go through the steps. We covered VPS creation, Docker install, Traefik v3.7 with automatic Let’s Encrypt TLS certificates, dashboard security with bcrypt auth, a socket proxy for reduced blast radius, and deploying your first app with Traefik labels.

The key improvements over a bare-bones setup: socket proxy limits Docker API exposure, acme.json with proper permissions prevents the most common beginner failure, healthchecks catch problems early, and bcrypt auth with the $$ escaping handled in Step 7 keeps the dashboard locked down.

Once Traefik is running, adding new services is just a matter of adding labels to a Docker container and putting it on traefik-net. Dockge makes managing those compose files even easier from a web UI.

Best Docker Containers for Your Home Server