Skip to main content

Command Palette

Search for a command to run...

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

A practical, battle-tested guide to wiring n8n, Traefik, Docker networks, and Let’s Encrypt into a production-ready stack.

Updated
8 min read
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.”

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 → 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

  • 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

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

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.webwebsecure 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:

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:

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:

      - "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.

    • Uses the websecure entrypoint (port 443).

    • Uses the letsencrypt resolver defined in traefik.yaml to get the certificate.

  • HTTP router + redirect middleware:

      - "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:

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:

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

And I got:

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") 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:

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

Result:

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:

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:

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:

docker compose down
docker compose up -d

Then:

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:

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

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/

Check:

  • Routers:

    • Do you see N8N and N8N-http?

    • Do they have Rule = Host("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

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:

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

Inside the container:

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

In logs:

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—you just add another container with a few Traefik labels. Traefik discovers it automatically; no restarts, no big config edits..