Skip to main content

Automatic TLS Certificates with Traefik and Let's Encrypt

Jin Li
Author
Jin Li
Fate lies within the lightcone.
Table of Contents
Reverse Proxy Series - This article is part of a series.
Part 3: This Article

Background
#

I was previously using Cloudflare Tunnel (free plan) to reverse proxy my Nextcloud container. While convenient, the free tier has a 100MB upload limitation that became problematic for my usage.

This article documents my migration from Cloudflare Tunnel to direct Traefik reverse proxy with Let’s Encrypt TLS certificates, including the complete setup, issues encountered, and troubleshooting steps.

Why Migrate?
#

Cloudflare Tunnel (Free Plan) Pros
#

  • ✅ Automatic SSL/TLS termination
  • ✅ No port forwarding needed
  • ✅ Works with dynamic IPs
  • ✅ Built-in DDoS protection

Cloudflare Tunnel (Free Plan) Cons
#

  • 100MB upload limit (major issue)
  • ❌ Bandwidth limits
  • ❌ Tunnel overhead adds latency
  • ❌ Dependency on Cloudflare infrastructure

Why Traefik + Let’s Encrypt
#

  • No upload limits (configurable)
  • ✅ Direct connection (lower latency)
  • ✅ Automatic certificate management
  • ✅ Full control over routing
  • ✅ Can handle multiple services

Current Architecture
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
┌────────────────────────────────────────────────────────────┐
│                      Internet                              │
└─────────────────────────────┬──────────────────────────────┘
                  ┌───────────▼───────────┐
                  │       Router            │
                  │    Port Forwarding      │
                  │  80, 443 → Server       │
                  └────────────┬────────────┘
                  ┌────────────▼───────────┐
                  │      Traefik            │
                  │   (TLS Termination)     │
                  └────────────┬────────────┘
                  ┌────────────▼───────────┐
                  │      Nextcloud          │
                  │      (Docker)           │
                  └─────────────────────────┘

Before: Internet → Cloudflare Tunnel → Traefik → Nextcloud
After: Internet → Traefik → Nextcloud

Prerequisites
#

  • Docker and docker-compose installed
  • Traefik container running
  • Domain name (e.g., nextcloud2.example.com)
  • Cloudflare account with domain DNS managed
  • Server with public IP (or dynamic DNS)

Step 1: Remove Cloudflare Tunnel
#

Stop the cloudflared container
#

1
2
cd /path/to/cloudflared
docker compose down

Update cloudflared config (optional)
#

If you want to keep cloudflared for other services, remove the Nextcloud entry from config.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ingress:
  # Remove these lines:
  # - hostname: "cloud.example.com"
  #   service: https://traefik:443
  #   originRequest:
  #     noTLSVerify: true
  # - hostname: "nextcloud2.example.com"
  #   service: https://traefik:443
  #   originRequest:
  #     noTLSVerify: true
  # Keep other entries...
  - service: http_status:404

Step 2: Update DNS in Cloudflare
#

For each domain you want to use with Traefik:

  1. Go to Cloudflare DashboardDNS
  2. Find the DNS record (e.g., nextcloud2.example.com)
  3. Change from Orange Cloud (Proxied) to Gray Cloud (DNS Only)
  4. Point to your server’s public IP address

Important: For Let’s Encrypt DNS challenge to work, traffic must go directly to your server, not through Cloudflare’s proxy.

Step 3: Configure Traefik’s Let’s Encrypt DNS Challenge
#

Update traefik.yml
#

Ensure you have the ACME configuration with Cloudflare DNS challenge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
certificatesResolvers:
  le:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

Set Cloudflare API Token
#

Create or update .env file in the traefik directory:

1
2
3
CF_API_KEY=your_cloudflare_api_token
CF_API_EMAIL=[email protected]
TZ=UTC

Note: For DNS challenge, you need a Cloudflare API Token (not Global API Key) with the following permissions:

  • Zone DNS: Edit zone DNS

Update docker-compose.yml
#

Ensure environment variables are passed to Traefik:

1
2
3
4
environment:
  - CF_API_KEY=${CF_API_KEY}
  - CF_API_EMAIL=${CF_API_EMAIL}
  - TZ=${TZ:-UTC}

Step 4: Configure Dynamic Router with TLS
#

Create a file in traefik/dynamic/ directory (e.g., nextcloud2.yml):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
http:
  routers:
    nextcloud2-router-http:
      rule: "Host(`nextcloud2.example.com`)"
      entryPoints:
        - web
      middlewares:
        - redirect-https
      service: nextcloud2-service

    nextcloud2-router-https:
      rule: "Host(`nextcloud2.example.com`)"
      entryPoints:
        - websecure
      tls:
        certResolver: le  # Important: specifies Let's Encrypt resolver
      service: nextcloud2-service
  
  services:
    nextcloud2-service:
      loadBalancer:
        servers:
          - url: "http://nextcloud:80"  # Nextcloud container name
        passHostHeader: true

  middlewares:
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true

Key Configuration Points
#

  1. Two routers needed:

    • HTTP router for redirect (port 80)
    • HTTPS router for actual traffic (port 443)
  2. certResolver: le is crucial - without this, Traefik uses its internal self-signed certificate

  3. passHostHeader: true - preserves the original Host header

Step 5: Restart Traefik
#

1
2
cd /path/to/traefik
docker compose up -d --force-recreate traefik

Traefik will:

  1. Watch for new config files
  2. Request Let’s Encrypt certificate via DNS challenge
  3. Create _acme-challenge.nextcloud2.example.com TXT record
  4. Wait for DNS propagation
  5. Verify ownership
  6. Download certificate

Step 6: Verify Setup
#

Check if certificate was issued
#

1
2
3
4
5
# View all certificates
cat /path/to/traefik/acme.json | python3 -m json.tool

# Check specific domain
cat /path/to/traefik/acme.json | grep -A 5 'nextcloud2'

Test HTTPS connection
#

1
2
3
4
5
6
7
8
# Verify certificate
echo | openssl s_client -connect nextcloud2.example.com:443 -servername nextcloud2.example.com 2>/dev/null | openssl x509 -noout -issuer -dates

# Verify domain matches
echo | openssl s_client -connect nextcloud2.example.com:443 -servername nextcloud2.example.com 2>/dev/null | openssl x509 -noout -subject -text | grep -E "(Subject:|DNS:)"

# Test connection
curl -I https://nextcloud2.example.com

Add More Domains
#

To add additional domains, simply create more config files:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Create config for another domain
cat > /path/to/traefik/dynamic/blog.yml <<'EOF'
http:
  routers:
    blog-router-http:
      rule: "Host(`blog.example.com`)"
      entryPoints:
        - web
      middlewares:
        - redirect-https
      service: blog-service

    blog-router-https:
      rule: "Host(`blog.example.com`)"
      entryPoints:
        - websecure
      tls:
        certResolver: le
      service: blog-service

  services:
    blog-service:
      loadBalancer:
        servers:
          - url: "http://blog:80"
        passHostHeader: true

  middlewares:
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true
EOF

Traefik auto-reloads every 10 seconds, so no restart needed.

Common Issues & Solutions
#

1. Certificate Shows as Self-Signed
#

Problem: Browser shows “Not Secure” or self-signed certificate warning

Solution: Add certResolver: le to your router config. Without this, Traefik uses its internal certificate.

2. DNS Challenge Failed
#

Problem: Certificate not being issued, errors in logs

Solution:

  • Verify Cloudflare API token has correct permissions
  • Check Traefik has access to API token via environment variables
  • Ensure DNS is not proxied through Cloudflare (gray cloud)
  • Check logs: docker logs traefik

3. Certificate Exists but Not Valid for Domain
#

Problem: Let’s Encrypt cert exists, but not for your domain

Solution: Traefik creates certificates on-demand. Simply access the domain and Traefik will automatically request the certificate.

4. HTTP to HTTPS Redirect Not Working
#

Problem: HTTP requests don’t redirect to HTTPS

Solution: Ensure you have both HTTP and HTTPS routers configured with the redirect middleware.

5. Nextcloud Shows “Trusted Domain Error”
#

Problem: Nextcloud doesn’t trust the new domain

Solution: Update Nextcloud’s trusted domains:

In nextcloud/docker-compose.yml, update:

1
2
environment:
  - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud2.example.com,cloud.example.com,previous-domain.com

Or in Nextcloud config (config/config.php):

1
2
3
4
5
'trusted_domains' => [
    'nextcloud2.example.com',
    'cloud.example.com',
    'previous-domain.com',
],

Advanced Configuration
#

Enable Debug Logging
#

1
2
3
# traefik.yml
log:
  level: DEBUG

Configure Certificate TTL
#

1
2
3
4
5
6
7
8
9
certificatesResolvers:
  le:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
      caServer: "https://acme-v02.api.letsencrypt.org/directory"  # Production
      # caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"  # Staging

Multiple Domains for Single Service
#

1
2
3
4
5
6
7
8
9
http:
  routers:
    multi-domain-router-https:
      rule: "Host(`domain1.com`) || Host(`domain2.com`) || Host(`domain3.com`)"
      entryPoints:
        - websecure
      tls:
        certResolver: le
      service: my-service

Monitoring and Maintenance
#

Check Certificate Expiry
#

1
2
3
4
5
6
7
8
# Script to check all certificates
cat /path/to/traefik/acme.json | python3 <<'EOF'
import json, sys, datetime
data = json.load(sys.stdin)
for domain in data['le']['Certificates']:
    cert = domain['certificate']
    print(f"Domain: {domain['domain']['main']}")
EOF

Manual Certificate Renewal
#

Let’s Encrypt certs auto-renew 30 days before expiry. To force renewal:

1
docker compose exec traefik traefik renew-certs

Backup acme.json
#

1
cp /path/to/traefik/acme.json /path/to/traefik/acme.json.backup-$(date +%Y%m%d)

Performance Comparison
#

MetricCloudflare TunnelTraefik + Let’s Encrypt
Upload Limit100MB ❌Unlimited ✅
LatencyHigher (tunnel overhead)Lower (direct) ✅
SSL CertsAutomatic ✅Automatic ✅
CostFree (limited)Free (unlimited) ✅
ConfigurationSimpleModerate
DDoS ProtectionBuilt-in ✅Must add separately

Conclusion
#

Migrating from Cloudflare Tunnel to Traefik with Let’s Encrypt was straightforward and provides:

  • No upload limitations
  • Better performance
  • Full control
  • Automatic certificate management

The key takeaways:

  1. Remove Cloudflare Tunnel for the domain
  2. Update DNS to DNS only (not proxied)
  3. Configure Traefik with ACME + Cloudflare DNS challenge
  4. Set certResolver: le in router config
  5. Restart Traefik to request certificates

This setup is ideal for self-hosted services and scales well as you add more domains.

References
#

Changelog
#

  • 2026-07-05 - Initial publication, documented the migration process
Reverse Proxy Series - This article is part of a series.
Part 3: This Article