# How I Put n8n Behind Traefik with Automatic HTTPS: A Real-World Traefik + n8n Setup

This all started with a pretty simple goal:

> “Run **n8n** on my server and expose it cleanly at  
> [`https://n-test.mahdishadi.me`.”](https://n-test.mahdishadi.me.xn--ivg)

I could have just exposed n8n’s port directly on the server and called it a day.  
But I wanted something more:

* Clean domain-based routing
    
* Proper HTTPS with Let’s Encrypt
    
* A setup that scales nicely when I add more services
    

So I went for a **cloud-native** approach with **Traefik** in front and n8n behind it in Docker.

Of course, it didn’t “just work.” There were 404s, 308s, ACME errors, missing volumes, and plenty of `docker exec` + `curl`.  
Here’s the full story of what I did, why I did it, and how I debugged the problems along the way.

## Why Traefik? And Why Cloud-Native Matters Here

### What is Traefik?

Traefik is an **edge router / reverse proxy** built for the cloud-native world.

Instead of manually editing a huge config file and defining all your backends, Traefik:

* Discovers services automatically (via Docker, Kubernetes, Consul, etc.)
    
* Configures routes based on **labels** or **Ingress** definitions
    
* Handles **Let’s Encrypt** and automatic certificate renewal
    
* Offers middlewares (redirects, auth, rate limiting, etc.)
    
* Ships with a web dashboard and metrics integration
    

### Why use it instead of just exposing n8n directly?

When you have more than one service, the “just expose a random port” approach gets ugly fast:

* You end up with URLs like `server-ip:5678`, `server-ip:9000`, etc.
    
* Managing HTTPS per service becomes painful.
    
* Security and organization become a mess.
    

With Traefik:

* You expose **only** Traefik to the internet (ports 80/443).
    
* All internal services (like n8n) live on a private Docker network.
    
* You route based on hostname (e.g. [`n-test.mahdishadi.me`](http://n-test.mahdishadi.me) → n8n).
    
* SSL is handled centrally, automatically.
    

### What does “cloud-native” buy us here?

* **Dynamic service discovery**  
    New containers with the correct labels are picked up automatically—no manual configuration reloads are required.
    
* **Configuration via labels**  
    Routing logic lives alongside the service definition (in `docker-compose.yml`), not in a giant central config file.
    
* **Automatic TLS**  
    Traefik talks to Let’s Encrypt via ACME, requests and renews certs, and stores them in a file you mount.
    
* **Better security & separation**  
    Only Traefik is exposed. n8n is reachable only over an internal Docker network.
    

## The Final Architecture

Here’s the architecture I aimed for:

* **Traefik**:
    
    * Listens on ports `80` and `443` for HTTP/HTTPS
        
    * Has a dashboard on port `8080`
        
    * Talks to Docker and automatically discovers services
        
    * Gets Let’s Encrypt certs and stores them in `acme.json`
        
* **n8n** :
    
    * Runs in Docker on a private network `frontend`
        
    * Does *not* expose its port directly to the host
        
    * Is accessible only via Traefik at [`https://n-test.mahdishadi.me`](https://n-test.mahdishadi.me)
        
* **Docker network**:
    
    * A shared network named `frontend` that both Traefik and n8n join
        

---

## Traefik Setup: EntryPoints, Docker Provider, and Let’s Encrypt

### docker-compose for Traefik

```yaml
services:
  traefik:
    image: traefik:v3.6
    container_name: traefik-demo
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./config/traefik.yaml:/etc/traefik/traefik.yaml:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - frontend
    restart: unless-stopped

networks:
  frontend:
    external: true
```

Key ideas:

* The Docker socket lets Traefik read labels on your containers.
    
* The config file is mounted as `traefik.yaml`.
    
* The `./letsencrypt` directory is mounted into the container as `/letsencrypt` for cert storage.
    
* Traefik is on the `frontend` network so it can reach n8n.
    

### `traefik.yaml` configuration

```yaml
global:
  checkNewVersion: false
  sendAnonymousUsage: false

log:
  level: DEBUG

api:
  dashboard: true
  insecure: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:             # redirect HTTP -> HTTPS
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix://var/run/docker.sock"
    exposedByDefault: false     
    network: frontend          

certificatesResolvers:
  letsencrypt:
    acme:
      email: "mahdishadi99@gmail.com"
      storage: "/letsencrypt/acme.json"
      httpChallenge:
        entryPoint: "web"
```

Why this config?

* `exposedByDefault: false`  
    Traefik does **not** automatically publish every container. Only containers with `traefik.enable=true` become reachable. This is safer and more explicit.
    
* `network: frontend`  
    Traefik will use the `frontend` Docker network when talking to containers. n8n is on the same network, so they can talk.
    
* `entryPoints.web` → `websecure` redirect  
    Everything on port 80 gets redirected to HTTPS on port 443. All public traffic ends up encrypted.
    
* `certificatesResolvers.letsencrypt` with `httpChallenge`  
    Traefik uses the HTTP-01 challenge on the entrypoint `web` (port 80) to obtain certificates and store them at `/letsencrypt/acme.json`.
    

On the host side, I created the ACME storage file:

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

---

## n8n Setup: Labels and Environment

Here’s the n8n stack I ended up with:

```yaml
version: "3.8"

networks:
  frontend:
    external: true  

services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.N8N.rule=Host(`n-test.mahdishadi.me`)"
      - "traefik.http.routers.N8N.entrypoints=websecure"
      - "traefik.http.routers.N8N.tls.certresolver=letsencrypt"

      - "traefik.http.routers.N8N-http.rule=Host(`n-test.mahdishadi.me`)"
      - "traefik.http.routers.N8N-http.entrypoints=web"
      - "traefik.http.routers.N8N-http.middlewares=N8N-https-redirect"
      - "traefik.http.middlewares.N8N-https-redirect.redirectscheme.scheme=https"

    environment:
      - N8N_HOST=n-test.mahdishadi.me
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_URL=https://n-test.mahdishadi.me
      - GENERIC_TIMEZONE=Asia/Tehran
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=********
      - N8N_USER_MANAGEMENT_DISABLED=true
      - N8N_AUTH_DISABLE=true
      - N8N_DIAGNOSTICS_ENABLED=false

    volumes:
      - n8ntest_data:/home/node/.n8n
      - ./local-files:/files
    networks:
      - frontend

volumes:
  n8ntest_data:
    external: true
```

### Why these Traefik labels?

* `traefik.enable=true`  
    Required because `exposedByDefault: false`. Without this, Traefik ignores the container completely.
    
* Main HTTPS router:
    
    ```yaml
    - "traefik.http.routers.N8N.rule=Host(`n-test.mahdishadi.me`)"
    - "traefik.http.routers.N8N.entrypoints=websecure"
    - "traefik.http.routers.N8N.tls.certresolver=letsencrypt"
    ```
    
    * Matches only requests with `Host:` [`n-test.mahdishadi.me`](http://n-test.mahdishadi.me).
        
    * Uses the `websecure` entrypoint (port 443).
        
    * Uses the `letsencrypt` resolver defined in `traefik.yaml` to get the certificate.
        
* HTTP router + redirect middleware:
    
    ```yaml
    - "traefik.http.routers.N8N-http.rule=Host(`n-test.mahdishadi.me`)"
    - "traefik.http.routers.N8N-http.entrypoints=web"
    - "traefik.http.routers.N8N-http.middlewares=N8N-https-redirect"
    - "traefik.http.middlewares.N8N-https-redirect.redirectscheme.scheme=https"
    ```
    
    This router listens on `web` (port 80) and applies the `N8N-https-redirect` middleware, which simply changes the scheme to `https`. That means:
    
    * [`http://n-test.mahdishadi.me`](http://n-test.mahdishadi.me) → 301/308 → [`https://n-test.mahdishadi.me`](https://n-test.mahdishadi.me).
        

### Why these n8n environment variables?

* `N8N_HOST`, `N8N_PORT`, `N8N_PROTOCOL`  
    n8n must know how it is accessed from the outside so it can generate correct URLs.
    
* `WEBHOOK_URL`  
    Used for webhook URLs n8n exposes; it must match the external HTTPS URL.
    
* `GENERIC_TIMEZONE=Asia/Tehran`  
    So scheduled workflows match your local time.
    
* `N8N_BASIC_AUTH_*`  
    Adds an extra Basic Auth layer in front of the n8n UI. (For a public internet-facing instance, this is very helpful.)
    
* `N8N_USER_MANAGEMENT_DISABLED`, `N8N_AUTH_DISABLE`  
    Disables n8n’s internal user management/auth, relying instead on the Basic Auth above for this scenario.
    
* `N8N_DIAGNOSTICS_ENABLED=false`  
    Disables telemetry/diagnostics.
    

---

## The Problems I Hit (and How They Were Fixed)

### Problem 1: 404 from Traefik – No Router Matched

One of the first tests I ran was:

```yaml
curl -v http://91.107.179.182/ -H 'Host: n-test.mahdishadi.me'
```

And I got:

```yaml
HTTP/1.1 404 Not Found
404 page not found
```

This means:

* The request *did* reach Traefik (good).
    
* But Traefik didn’t find any router that matched `Host("`[`n-test.mahdishadi.me`](http://n-test.mahdishadi.me)`")` on that entrypoint (bad).
    

Possible causes I checked:

* Wrong or missing labels
    
* Docker provider is not working correctly
    
* Service isn’t on the same network as Traefik
    

By fixing the Docker provider config (`exposedByDefault`, `network: frontend`) and ensuring both Traefik and n8n were attached to the `frontend` network, Traefik started picking up the routers correctly.

### Problem 2: HTTP → HTTPS Worked (308), but HTTPS Still Failed

After adding the HTTP router and redirect middleware, I tested:

```yaml
curl -I http://n-test.mahdishadi.me
```

Result:

```yaml
HTTP/1.1 308 Permanent Redirect
Location: https://n-test.mahdishadi.me/
```

So the HTTP side looked great:

* `N8N-http` router was active.
    
* Middleware `N8N-https-redirect` did its job.
    

But the next step was making sure **HTTPS itself** worked with a valid certificate. That’s where Let’s Encrypt and ACME came in.

---

### Problem 3: Let’s Encrypt / ACME Errors – “HTTP challenge not enabled” / Resolver Skipped

I configured ACME in `traefik.yaml` and created `letsencrypt/acme.json` with permissions `600`.  
Everything *looked* correct… but Traefik logs / dashboard complained:

* The HTTP challenge wasn’t enabled, or
    
* The `letsencrypt` Resolver was being skipped.
    

So I jumped into the container to see if the ACME storage path really existed:

```yaml
docker exec -it traefik-demo sh
/ # ls /letsencrypt
ls: can't access '/letsencrypt': No such file or directory
```

Boom. There it was.

### Root cause: the volume wasn’t mounted (old container, new compose)

Yes, my `docker-compose.yml` had:

```yaml
volumes:
  - ./letsencrypt:/letsencrypt
```

But I had **not** recreated the container after adding that. I had only restarted it.  
Docker will not magically pick up new volumes on an existing container; you must recreate it.

The fix:

```yaml
docker compose down
docker compose up -d
```

Then:

```yaml
docker exec -it traefik-demo sh
/ # ls /letsencrypt
acme.json
/ # ls -l /letsencrypt/acme.json
-rw------- 1 root root 0 Nov 28 05:51 acme.json
```

Now Traefik could actually access `/letsencrypt/acme.json`, and the ACME resolver started working properly. Once the certificate was obtained, `acme.json` grew from 0 bytes to a real JSON file with cert data.

At that point, hitting:

```yaml
curl -vk https://n-test.mahdishadi.me/
```

returned the n8n HTML with a valid Let’s Encrypt certificate, and browsers showed a secure connection.

---

## Debugging Traefik + n8n: A Practical Checklist

This journey turned into a pretty solid debugging checklist:

### 1\. Always start with `curl` + `Host` header

```yaml
curl -v http://SERVER_IP/ -H 'Host: n-test.mahdishadi.me'
curl -vk https://n-test.mahdishadi.me/ -H 'Host: n-test.mahdishadi.me'
```

What errors mean:

* **404** → No matching Traefik router (rule/labels/entrypoint issue).
    
* **308** → HTTP redirect is working.
    
* **502** → Router exists, but backend (n8n) is unreachable (network/port/container down).
    

### 2\. Use the Traefik dashboard

Visit:  
[`http://SERVER_IP:8080/dashboard/`](http://SERVER_IP:8080/dashboard/)

Check:

* **Routers**:
    
    * Do you see `N8N` and `N8N-http`?
        
    * Do they have `Rule = Host("`[`n-test.mahdishadi.me`](http://n-test.mahdishadi.me)`")`?
        
    * Are the entrypoints `web` / `websecure` as expected?
        
* **Services**:
    
    * Is there a service backing `N8N`?
        
    * Is it green (healthy) or red (error)?
        
    * What internal IP/port is Traefik using?
        

### 3\. Verify Docker networking

```yaml
docker network inspect frontend
docker inspect traefik-demo | grep -A3 '"frontend"'
docker inspect n8n | grep -A3 '"frontend"'
```

Both containers must be attached to the same network (`frontend`) or Traefik can’t reach n8n.

### 4\. Double-check ACME / Let’s Encrypt

On the host:

```yaml
ls -l letsencrypt
# acme.json should be -rw------- (chmod 600)
```

Inside the container:

```yaml
docker exec -it traefik-demo sh
ls -l /letsencrypt/acme.json
```

In logs:

```yaml
docker logs -f traefik-demo | grep -i acme
```

Look for:

* Certificates being requested / obtained
    
* No “resolver skipped” or “challenge not enabled” errors
    

## Why This Architecture Makes Sense for n8n

In the end, this setup gives you:

* A **single, secure entrypoint** (Traefik on ports 80/443).
    
* **n8n hidden** behind a Docker network (`frontend`), not exposed directly to the internet.
    
* **Automatic, renewable HTTPS** via Let’s Encrypt.
    
* **Forced HTTPS** (HTTP → HTTPS redirect).
    
* Routing defined **right next to the service** (as Docker labels) instead of a giant reverse proxy config.
    
* n8n correctly configured with its external URL and secured with Basic Auth.
    

And the best part: when you want to add another service—say, an API at [`api.mahdishadi.me`](http://api.mahdishadi.me)—you just add another container with a few Traefik labels. Traefik discovers it automatically; no restarts, no big config edits..
