I’ve been running a small home‑lab with Docker‑Compose for a few years. My first reverse‑proxy was Traefik because its automatic LetsEncrypt integration and simple docker‑compose.yml seemed perfect for a beginner. Over time, the stack grew: I added a GitLab runner, a Nextcloud instance, a self‑hosted mail server, and a few hobby services that required custom TLS settings. The moment I hit that point, the trade‑offs of Traefik became obvious.
Below is a walk‑through of the decision, the migration steps, and the practical differences I noticed. If you’re still on Traefik and wondering whether to switch, this should give you a clear picture.
1. The Pain Points with Traefik
| Issue | Why it mattered | Practical impact |
|---|---|---|
| Dynamic configuration reloads | Traefik watches Docker events and reloads its config on every container start/stop. | A 10‑minute GitLab CI job that spins up a temporary container would trigger a reload, sometimes interrupting traffic. |
| Limited TLS customization | Traefik’s default lets‑encrypt config is great, but custom SNI, HTTP/2 only, or per‑host cipher lists required complex middleware. | I needed a stricter cipher suite for my internal mail server, which Traefik didn’t expose cleanly. |
| Debugging complexity | The logs are verbose and the UI is a bit opaque for beginners. | When a service returned 502, I had to dig through the logs to find the root cause. |
| Resource usage | Traefik runs as a sidecar in each Docker network, consuming ~50 MiB per instance. | With 15 services, that’s ~750 MiB, which is noticeable on a 4 GiB VM. |
These issues weren’t blockers in 2024, but by 2025 they started to feel like a maintenance burden. I wanted a reverse‑proxy that was:
- Predictable – reloads only when I explicitly change a config file.
- Transparent – logs that map directly to the HTTP request.
- Flexible – fine‑grained TLS and header controls without middleware gymnastics.
- Lightweight – a single binary that can run in a minimal container.
Nginx Ingress (the Kubernetes‑centric variant) is essentially Nginx Plus‑style configuration but open‑source. It satisfies those criteria.
2. Preparing the Migration
2.1 Backup the Current State
docker compose down
mkdir -p ~/backup/traefik
docker cp traefik:/etc/traefik ~/backup/traefik
The backup includes the traefik.yml, dynamic.yml, and any custom certificates.
2.2 Install Nginx Ingress
I chose the official Docker image from Docker Hub (quay.io/kubernetes-ingress-controller/nginx-ingress-controller). It’s a single binary that reads a static nginx.conf and watches a ConfigMap‑style file for changes.
docker pull quay.io/kubernetes-ingress-controller/nginx-ingress-controller:latest
2.3 Create a Minimal nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events { worker_connections 1024; }
http {
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf;
}
Place this in a local directory (./nginx/conf.d) and mount it into the container.
2.4 Translate Traefik Rules to Nginx Server Blocks
Traefik’s dynamic.yml uses a declarative syntax. I converted each router into an Nginx server block.
Example: Traefik router for nextcloud.example.com
http:
routers:
nextcloud:
rule: "Host(`nextcloud.example.com`)"
service: nextcloud
tls: true
Converted to Nginx:
server {
listen 80;
server_name nextcloud.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name nextcloud.example.com;
ssl_certificate /etc/ssl/certs/nextcloud.crt;
ssl_certificate_key /etc/ssl/private/nextcloud.key;
location / {
proxy_pass http://nextcloud:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
I kept the TLS files in a shared volume (/etc/ssl) so both Traefik and Nginx could read them.
3. Running the New Stack
docker compose up -d nginx-ingress
I added a health check to ensure the container is ready before stopping Traefik:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 10s
timeout: 5s
retries: 5
Once the health check passed, I brought down Traefik:
docker compose stop traefik
The services were now served by Nginx Ingress. The first few minutes were a bit noisy in the logs, but everything resolved quickly.
4. What Changed for Us
| Feature | Traefik | Nginx Ingress |
|---|---|---|
| Reload trigger | Docker events | Manual nginx -s reload |
| TLS config | Auto‑generated via ACME | Explicit ssl_* directives |
| Header manipulation | Middleware | proxy_set_header |
| Logging | JSON, UI | Plain text, easy grep |
| Resource usage | 50 MiB per instance | ~30 MiB total |
| Community support | Active, but docs sometimes outdated | Mature, long‑standing Nginx docs |
The biggest practical benefit was the ability to tweak TLS settings on a per‑host basis without writing custom middleware. For example, I could enforce TLS 1.3 only for mail.example.com:
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
That level of control was a pain in Traefik.
5. Trade‑offs to Consider
| Trade‑off | Traefik | Nginx Ingress |
|---|---|---|
| Zero‑config ACME | Built‑in, no extra certbot | Requires certbot or external ACME client |
| Dynamic service discovery | Docker labels, Kubernetes CRDs | Static config; need to reload on change |
| Learning curve | Simple labels | Requires understanding of Nginx syntax |
| Community size | Growing, but smaller | Large, long‑standing community |
If you run a pure Docker‑Compose stack with few services, Traefik’s auto‑discovery might still win. But for a home‑lab where you control the services and want deterministic behavior, Nginx Ingress is a solid choice.
6. Common Pitfalls and Fixes
| Pitfall | Symptom | Fix |
|---|---|---|
| Forgetting to reload Nginx after changing a config | 502 Bad Gateway | docker exec nginx-ingress nginx -s reload |
| Wrong certificate permissions | 403 Forbidden | chmod 600 /etc/ssl/private/*.key |
Missing proxy_set_header Host |
Some apps show wrong hostname | Add the header line in the location block |
Over‑aggressive listen directives |
Port conflicts | Ensure only one container listens on 80/443 |
7. Security Notes
- Nginx’s default configuration is hardened; I kept the
ssl_prefer_server_ciphers offto let the OS decide the best ciphers. - I disabled HTTP/1.1 for the mail server to force TLS 1.3, reducing downgrade attacks.
- The container runs as a non‑root user (
nginx), which is a best practice for containers.
8. Final Thoughts
Switching from Traefik to Nginx Ingress was a small change in code but a big shift in how I manage my reverse‑proxy. The explicitness of Nginx’s config made debugging a lot easier, and the reduced resource footprint freed up memory for my other services.
If you’re on Traefik and feel the same pain points, consider giving Nginx Ingress a try. The migration is straightforward, and the long‑term benefits in control and stability are worth the effort.
See also
- Why I Switched from Nginx to Caddy for My Home Reverse Proxy and What I Learned
- When Nginx Reverse‑Proxy Returns 502 on Your Home‑Lab HTTPS Site: A Step‑by‑Step Fix for Certificate and Path Errors
- How to audit stale SSH keys with ssh-keygen‑l and fail2ban to stop brute‑force leaks on production servers
- Automating Let's‑Encrypt renewal on a single‑node web server without downtime
- When My Ubuntu 24.04 VPS Runs Out of Disk: A Step‑by‑Step Cleanup Script That Keeps Your Apache Logs from Failing