Why I Swapped Traefik for Nginx Ingress in My Home Reverse‑Proxy Stack.

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 off to 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