Skip to main content
Free SSL Without Port 80 – Traefik + Cloudflare DNS-01

Free SSL Without Port 80 – Traefik + Cloudflare DNS-01

Zarvelion Zynji
Author
Zarvelion Zynji
Tech enthusiasts (self-proclaimed). Gaming addict (diagnosed). Anime simp (no regrets). I turn my hyperfixations into content—welcome to the chaos.
Table of Contents

Lock Down Your Homelab With Free SSL — No Port 80 Required
#

That red “Not Secure” badge next to your homelab dashboard URL isn’t just ugly — it’s a real risk. Credentials submitted over plain HTTP travel unencrypted, and any device on your LAN can intercept them. But the standard fix has its own problem: Let’s Encrypt’s HTTP-01 challenge expects Port 80 open to the internet, and most residential ISPs block it outright. Even if yours doesn’t, punching that hole in your firewall is a trade-off you shouldn’t have to make.

There’s a better path. The DNS-01 challenge proves domain ownership through a TXT record in DNS — no inbound ports, no HTTP exposure. Pair it with Traefik as your reverse proxy and Cloudflare as your DNS provider, and you get automatic certificate issuance and renewal with zero manual Certbot maintenance.

Here’s how to set it up from scratch.


What You’ll Need
#

Requirement Why
A server running Docker & Docker Compose Hosts Traefik and your apps
A personal domain (e.g. example.com) on Cloudflare DNS-01 needs Cloudflare’s API to create TXT records
A Cloudflare API Token with Zone → DNS → Edit permission Traefik uses this to automate the challenge
(Optional) Pi-hole or local DNS resolver Resolves your domain to local IPs so you skip public round-trips

How DNS-01 Beats HTTP-01 for Homelabs
#

Standard Let’s Encrypt flow (HTTP-01):

  1. Let’s Encrypt tells you to serve a token at http://yourdomain/.well-known/acme-challenge/...
  2. Their CA connects to your public IP on Port 80 to verify it.
  3. Certificate issued.

Three dealbreakers for homelabs:

  • ISP blocks Port 80 — most residential providers do.
  • Port forwarding exposes a service port to the entire internet.
  • CGNAT — carrier-grade NAT makes your home unreachable anyway.

DNS-01 flow:

  1. Let’s Encrypt tells you to publish a TXT record at _acme-challenge.yourdomain.
  2. Their CA reads the record from DNS — no connection to your network.
  3. Certificate issued.

No ports. No inbound traffic. Works behind CGNAT. Works behind double NAT. Works everywhere.


Step 1 — Create the ACME Storage File
#

Traefik stores Let’s Encrypt private keys in acme.json. It must exist before launch and be locked down:

mkdir -p /opt/traefik
touch /opt/traefik/acme.json
chmod 600 /opt/traefik/acme.json

The 600 permission restricts read/write to the file owner only. Your SSL private keys live here — treat it like a secret.


Step 2 — Write the Traefik Docker Compose
#

Create /opt/traefik/docker-compose.yml:

version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"     # HTTP entrypoint (internal redirect only)
      - "443:443"   # HTTPS entrypoint
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/acme.json
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"

Key lines explained:

Line Purpose
docker.sock:ro Lets Traefik discover containers (read-only — safe)
CF_DNS_API_TOKEN Cloudflare API token for DNS-01 TXT record automation
dnschallenge=true Selects DNS-01 instead of HTTP-01 or TLS-ALPN-01
provider=cloudflare Tells ACME which DNS plugin to use
storage=/acme.json Persistent cert/key store across restarts
web → websecure redirect Forces all HTTP to HTTPS automatically

Put your token in a .env file next to the compose file:

echo 'CF_DNS_API_TOKEN=your_cloudflare_api_token_here' > /opt/traefik/.env

Launch Traefik:

docker compose up -d

Step 3 — Attach SSL to Your Apps
#

Traefik reads Docker labels to know which containers need TLS. Add these four labels to any service you want secured:

services:
  homer:
    image: b4bz/homer:latest
    container_name: homer
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.homer.rule=Host(`dash.example.com`)"
      - "traefik.http.routers.homer.entrypoints=websecure"
      - "traefik.http.routers.homer.tls.certresolver=letsencrypt"
    networks:
      - proxy

networks:
  proxy:
    external: true

Label breakdown:

  • traefik.enable=true — opts this container into Traefik routing.
  • Host(...) — matches requests for dash.example.com.
  • entrypoints=websecure — listens on the HTTPS entrypoint (Port 443).
  • tls.certresolver=letsencrypt — triggers certificate issuance via the letsencrypt resolver.

The first request to https://dash.example.com will pause briefly while Let’s Encrypt issues the certificate. Subsequent visits are instant. Renewals happen automatically ~30 days before expiry — no cron, no manual certbot renew.

Every app you add only needs these four labels. That’s the power of centralized cert management with Traefik.


Step 4 — (Optional) Local DNS Resolution
#

Right now dash.example.com resolves to your Cloudflare public IP, which may point nowhere useful if you’re only accessing services locally. Two options:

Option A — Pi-hole Local DNS
#

Add a Local DNS Record in Pi-hole:

  • Domain: dash.example.com
  • IP: 192.168.x.x (your server’s LAN IP)

Option B — /etc/hosts Override
#

On each client machine:

echo '192.168.x.x  dash.example.com' | sudo tee -a /etc/hosts

Either way, your browser connects directly on LAN with a valid SSL certificate — best of both worlds.


Troubleshooting
#

Symptom Cause Fix
502 Bad Gateway App and Traefik on different Docker networks Add both to the same proxy network
Certificate not issued Invalid or missing Cloudflare API token Verify token has Zone → DNS → Edit scope; check Traefik logs with docker logs traefik
Challenge timeout Cloudflare propagation delay (rare) Wait 1-2 minutes and reload; DNS-01 usually propagates in seconds
“Not Secure” after cert issued Browser caching HTTP response Hard-refresh (Ctrl+Shift+R) or clear cache

FAQ
#

Does DNS-01 work if my server has no public IP at all? Yes. DNS-01 verification happens entirely at the DNS layer. Let’s Encrypt never connects to your server. This makes it ideal for CGNAT, double-NAT, and air-gapped-egress setups.

Do I need to forward Port 443 either? No. Port 443 mapping in the compose file is for local access — your browser on the LAN connects to https://dash.example.com:443. If you later expose services publicly (e.g. through a Cloudflare Tunnel), you still don’t need port forwarding.

What happens when the certificate expires? Nothing you do manually. Traefik’s ACME client renews certificates automatically roughly 30 days before expiry. As long as the Traefik container is running, renewal is hands-off.

Can I use a different DNS provider? Yes — Traefik supports dozens via lego: Route53, DigitalOcean, Google Cloud DNS, Hetzner, and more. Change provider=cloudflare and the corresponding environment variable.

Why not just use Cloudflare’s origin certificate? Cloudflare origin certificates are only trusted by Cloudflare’s proxy. If you access your domain directly (bypassing Cloudflare), the browser rejects the cert. Let’s Encrypt certs are trusted everywhere.


Recap
#

Before After
HTTP dashboards, browser warnings Valid Let’s Encrypt SSL on every app
Port 80 forwarded to the internet Zero inbound ports
Manual certbot renew every 60-90 days Fully automatic issuance and renewal
One cert per app, per process Centralized — add 4 labels to any container

Three files: acme.json (chmod 600), docker-compose.yml, .env. Four labels per app. That’s the entire setup.

Related


Load Comments