Traefik with Docker: A Reverse Proxy That Discovers Your Containers

Traefik reads Docker container labels and builds routing and Let's Encrypt certificates automatically, so adding a new app with its own domain and certificate takes a few lines in docker-compose without touching or restarting the proxy.
The short answer
Traefik is a reverse proxy designed for the container world. It connects to Docker, watches containers start and stop, reads labels on each container that declare its domain, port and policies, and builds the routing on its own within seconds. There is no central config file to edit for every app and no manual reload. You add five lines of labels to a new service in docker-compose, start it, and it has a domain and a valid Let's Encrypt certificate in under a minute.
Choose Traefik when you run several containerized applications on one or more servers and they change often: staging environments that come and go, internal tools such as n8n and Open WebUI, and client applications. If you have one or two static sites, or need very fine control over caching and static file serving, Nginx remains stronger, and Caddy is simpler if you want a short config file without Docker labels.
Protection in Traefik comes from middlewares you attach to any route: rate limiting, basic authentication, security headers and IP allow lists. The built-in dashboard is useful, but it must always sit behind authentication and an IP allow list.
Traefik, Nginx or Caddy
| Criterion | Traefik | Nginx | Caddy |
|---|---|---|---|
| Container discovery | Automatic through labels | Manual or extra tooling | Through a third-party plugin |
| HTTPS certificates | Built in, automatic | Through Certbot | Built in, automatic |
| Reload on change | Not needed | Manual reload | API or reload |
| Static file serving | Not its focus | Strongest | Good |
| Typical memory use | 50 to 100MB | 10 to 30MB | 30 to 60MB |
| Best fit | Many changing containers | High-traffic static sites | A few sites, simple config |
The memory gap is irrelevant on a server with 4GB or more. The real difference is effort: with Nginx every new app means a server block, a certificate and a reload, while with Traefik it means labels in the app's own compose file, so the routing configuration lives next to the code it belongs to.
Base setup for the Traefik container
Run Traefik as its own container in a dedicated compose file and configure it through command-line arguments or traefik.yml with these values:
- Two entry points, web on port 80 and websecure on 443, with every HTTP request redirected to HTTPS at the entry point level.
- The Docker provider with exposedByDefault=false, so no container is published unless it explicitly asks with traefik.enable=true. This setting alone prevents accidentally exposing a database or an admin panel.
- A certificate resolver named le using ACME with the TLS or HTTP challenge, an email address for notices, and an acme.json store with 600 permissions on a persistent volume. Otherwise you request fresh certificates on every restart until you hit Let's Encrypt rate limits.
- A shared external Docker network called proxy that Traefik and every published container join, while databases stay on internal networks private to each app.
Traefik needs to read the Docker socket to see containers, and that is sensitive access, since whoever holds the socket effectively owns the server. Mount it read-only at minimum, and preferably put a docker-socket-proxy in front that allows read requests only, so a compromised Traefik container cannot create new containers.
Pin the image version exactly, such as traefik:v3.1 rather than latest. Major upgrades have renamed settings before, and a silent upgrade in the middle of the night can take every app down at once.
Labels for routing multiple domains
For each service you want to publish, add these labels to its docker-compose file, replacing the name erp with a unique name per app:
- traefik.enable=true to turn publishing on.
- traefik.http.routers.erp.rule=Host(`erp.example.sa`) to set the domain, and you can combine several domains with ||.
- traefik.http.routers.erp.entrypoints=websecure and traefik.http.routers.erp.tls.certresolver=le for the automatic certificate.
- traefik.http.services.erp.loadbalancer.server.port=8080 to tell Traefik the app's internal port.
Notice that you never publish the app's port on the server, so the service has no ports section at all. Traefik reaches it over the internal proxy network, and port 8080 stays invisible from the internet. If you run several replicas of the same service, Traefik load balances across them automatically.
Middlewares for protection
A middleware is defined once in the labels of any container and then attached to any router by name. Shared ones are best defined on the Traefik container itself.
Rate limiting
ratelimit.average=100 and ratelimit.burst=50 mean an average of 100 requests per second per IP address, with an extra burst of 50 allowed. For login pages drop it to 5 or 10, which stops password guessing without regular users noticing anything.
Basic authentication
basicauth.users takes a username with an htpasswd password hash. The hash contains dollar signs, and in docker-compose each one must be doubled as $$, otherwise parts of the hash vanish and the password will not work. Use it for internal tools only, never as a replacement for the application's own login.
Security headers and IP lists
A headers middleware adds stsSeconds=31536000 to enforce HTTPS for a full year, plus frameDeny=true and contentTypeNosniff=true. An ipallowlist.sourcerange middleware restricts access to your office addresses or VPN range, which suits admin panels best.
Securing the dashboard
The Traefik dashboard shows every router, service and middleware with its status, which is very handy when chasing a routing bug. Enable it with api.dashboard=true, and never use api.insecure=true, which opens it on port 8080 with no protection at all. Publish it instead through a regular router on a domain like traefik.example.sa, using the service api@internal, and attach both the basic auth and IP allow list middlewares. Two layers, because the dashboard reveals your entire infrastructure map to anyone who sees it.
Troubleshooting the usual failures
Three errors show up for almost everyone starting with Traefik. The first is a 404 on a new domain: usually traefik.enable=true is missing, the container is not on the proxy network, or the same router name is used in two apps so one overrides the other. The second is a 502 Bad Gateway: the port in loadbalancer.server.port does not match the port the app listens on inside the container, or the app listens on 127.0.0.1 instead of 0.0.0.0. The third is a placeholder certificate named TRAEFIK DEFAULT CERT: the DNS record does not point at the server yet, or port 80 is closed so the challenge fails.
Turn on accesslog in JSON format with the log level at INFO while you get started, so every request appears with the router that caught it, its latency and its status code. For ongoing monitoring, enable the built-in Prometheus metrics and watch the share of 5xx responses per service. If it passes 1% over 5 minutes, an app needs your attention.
A full example: three apps on one server
A common setup for a mid-sized company: an internal system built with Laravel, the n8n automation tool, and Open WebUI as an internal AI assistant. The layout on a single server:
- Traefik in its own compose file on the proxy network, listening on 80 and 443.
- The Laravel system on erp.example.sa, internal port 8000, with its database on an internal network that never joins proxy, and a rate limit of 10 requests per second on the login path.
- n8n on n8n.example.sa, internal port 5678, behind an office-only IP list, with the webhook path carved out by a second router without the IP list so external calls still arrive.
- Open WebUI on ai.example.sa, internal port 8080, using its own login plus the security headers.
Each app lives in its own folder with its own compose file, so updating n8n never touches the other two and Traefik needs no change. When you add a fourth app later, you write its compose file with labels, start it, and you are done.
As for resources, Traefik takes about 80MB, Laravel with MySQL about 1GB, n8n 300 to 500MB, and Open WebUI about 500MB when connected to a model over an API. The Growth plan (2 vCPU, 4GB RAM) at 6,900 SAR a year fits that total with reasonable headroom. If you want to run local models through Ollama, the Business plan (4 vCPU, 8GB RAM) at 11,500 SAR a year is the practical minimum. n8n and Open WebUI come preinstalled as templates on AI hosting, and all resources are listed on the Cloud Server page.
Getting started
- Create the proxy network and start Traefik with exposedByDefault=false and a persistent acme.json at 600 permissions.
- Move one app first, remove its ports section, and confirm its certificate is issued within a minute.
- Define the security and rate limit middlewares once and attach them to every new router.
- Publish the dashboard behind authentication and an IP list, or leave it disabled if you do not need it.


