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.

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:
- Automatic service discovery and configuration
- Support for multiple protocols (HTTP, HTTPS, TCP, UDP)
- Built-in monitoring dashboard
- Built-in Let’s Encrypt integration for automatic SSL/TLS certificate management
- Support for various load balancing algorithms
- Middleware for adding extra functionality like authentication or rate limiting

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.
Create a compose.yml file:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy:latest
container_name: socket-proxy
restart: unless-stopped
networks:
- traefik-net
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: 1
SERVICES: 1
TASKS: 1
NETWORKS: 1
security_opt:
- no-new-privileges:true
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
command:
#- --log.level=DEBUG
- --api.dashboard=true
- --ping=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.endpoint=tcp://socket-proxy:2375
- --providers.docker.network=traefik-net
- --entrypoints.http.address=:80
- --entrypoints.http.http.redirections.entrypoint.to=https
- --entrypoints.http.http.redirections.entrypoint.scheme=https
- --entrypoints.https.address=:443
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
#- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- [email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
security_opt:
- no-new-privileges:true
networks:
- traefik-net
ports:
- 80:80
- 443:443
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 5s
retries: 3
env_file: .env
volumes:
- ./letsencrypt:/letsencrypt
labels:
- traefik.enable=true
- traefik.http.routers.traefik-secure.rule=Host(`traefik.yourdomain.com`)
- traefik.http.routers.traefik-secure.entrypoints=https
- traefik.http.routers.traefik-secure.service=api@internal
- traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt
- traefik.http.routers.traefik-secure.middlewares=traefik-auth
- traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}
- traefik.http.routers.traefik-secure.tls=true
networks:
traefik-net:
external: trueIf you prefer the simpler setup without a socket proxy, create a compose.yml file:
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
command:
#- --log.level=DEBUG
- --api.dashboard=true
- --ping=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=traefik-net
- --entrypoints.http.address=:80
- --entrypoints.http.http.redirections.entrypoint.to=https
- --entrypoints.http.http.redirections.entrypoint.scheme=https
- --entrypoints.https.address=:443
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
#- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- [email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
security_opt:
- no-new-privileges:true
networks:
- traefik-net
ports:
- 80:80
- 443:443
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 5s
retries: 3
env_file: .env
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
labels:
- traefik.enable=true
- traefik.http.routers.traefik-secure.rule=Host(`traefik.yourdomain.com`)
- traefik.http.routers.traefik-secure.entrypoints=https
- traefik.http.routers.traefik-secure.service=api@internal
- traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt
- traefik.http.routers.traefik-secure.middlewares=traefik-auth
- traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}
- traefik.http.routers.traefik-secure.tls=true
networks:
traefik-net:
external: trueThis mounts the Docker socket read-only directly into the Traefik container. It works fine for single-user setups, but the socket proxy version above limits blast radius if Traefik is compromised.
Command options explained:
--api.dashboard=true: Enables the Traefik web dashboard.--providers.docker=true: Enables Docker as a provider for automatic service discovery.--providers.docker.exposedbydefault=false: Prevents Traefik from automatically exposing all containers. You must explicitly enable each one.--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.)--providers.docker.network=traefik-net: Tells Traefik which Docker network to use for routing traffic to containers.--entrypoints.http.address=:80: HTTP entrypoint on port 80.--entrypoints.http.http.redirections.entrypoint.to=https: Redirect all HTTP traffic to HTTPS.--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.--entrypoints.https.address=:443: HTTPS entrypoint on port 443.--certificatesresolvers.letsencrypt.acme.tlschallenge=true: Enables the TLS-ALPN-01 challenge for Let’s Encrypt certificate acquisition.[email protected]: Your email for Let’s Encrypt registration and expiry notifications.--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json: Where to store certificates. This file needs specific permissions (covered in Step 9).--ping=true: Enables the healthcheck endpoint used by Docker healthcheck.
Dashboard labels explained:
traefik.enable=true: Enables Traefik for this container.traefik.http.routers.traefik-secure.rule=Host(traefik.yourdomain.com): Routes requests for this hostname to the dashboard.traefik.http.routers.traefik-secure.entrypoints=https: Uses the HTTPS entrypoint.traefik.http.routers.traefik-secure.service=api@internal: Routes to Traefik’s internal dashboard API.traefik.http.routers.traefik-secure.tls.certresolver=letsencrypt: Uses Let’s Encrypt for TLS certificates.traefik.http.routers.traefik-secure.middlewares=traefik-auth: Applies the auth middleware.traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}: Sets up Basic Auth. Credentials come from the.envfile. More on Traefik Basic Authentication.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:
-
Check container status:
docker ps: the traefik container should show “healthy” (the healthcheck takes ~30 seconds). -
Check logs for cert issuance:
docker logs traefik
Look for a line about certificate being obtained. No errors about acme.json or permissions.
- Test HTTP→HTTPS redirect:
curl -I http://traefik.yourdomain.com
Expect a 301 Moved Permanently redirecting to HTTPS.
- 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.
- Access the dashboard: Open
https://traefik.yourdomain.comin 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 traefikAlso 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:
- DNS not propagated: Verify with
dig traefik.yourdomain.com +short: it must return your server IP. - Port 443 not reachable: The TLS-ALPN-01 challenge requires inbound connections on port 443. Check your firewall and cloud provider security groups.
- 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/directoryStaging 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:
- The container is on the
traefik-netnetwork:
docker network inspect traefik-netYour container should appear in the output.
- The container has the
traefik.enable=truelabel:
docker inspect your-container | grep -i traefik.enableDashboard 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 .envMake 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
.envfiles in production - Keep Traefik updated.
traefik:v3.7tracks 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.


