Last active
September 10, 2026 20:43
-
-
Save fatihyildizhan/8f124039a9bd3801f0caf3c01c3601fb to your computer and use it in GitHub Desktop.
Traefik v3 + v2 + v1 + Docker + Let's Encrypt + GitHub Container Registry ghcr.io + Updated on 4 September 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Traefik v3 + v2 + v1 + Docker + Let's Encrypt + GitHub Container Registry ghcr.io + Updated on 4 September 2026 | |
| Content: | |
| - Ubuntu 26.04 LTS / 24.04 LTS | |
| - Docker Engine 29.8.0 | |
| - Docker Compose v5.5.1 (docker compose plugin) | |
| - Traefik v3.7 with httpChallenge / tlsChallenge / dnsChallenge <-- USE THIS FOR NEW SETUPS | |
| - Traefik v2.11 with httpChallenge <-- legacy, security fixes only | |
| - Traefik v1.7 with dnsChallenge <-- EOL since 2019, keep for old servers | |
| - GitHub Container Registry ghcr.io | |
| - Let's Encrypt | |
| Which version should I pick? | |
| - v3 -> Every new server. Actively developed, HTTP/3, simpler config, one router per service instead of two. | |
| - v2 -> Only if you already run v2 in production and cannot migrate today. Still gets security fixes, no new features. | |
| - v1 -> End of life since 2019, no fixes at all. Kept here only so old servers stay documented. Do not start new projects with it. | |
| TL DR: | |
| - Line 28 -> 0. Common setup: server, Docker, network. Do this first for every version. | |
| - Line 76 -> 1. Traefik v3 configs (recommended) | |
| - Line 306 -> 2. Traefik v2 configs (legacy) | |
| - Line 424 -> 3. Traefik v1 configs (EOL) | |
| - Line 541 -> 4. ghcr.io login, first container, troubleshooting | |
| - Line 582 -> 5. v2 -> v3 migration checklist | |
| ================================================================================ | |
| 0. COMMON SETUP (do this once, for any Traefik version) | |
| ================================================================================ | |
| 1. Create an Ubuntu 26.04 LTS (or 24.04 LTS) droplet / VM | |
| 1.1. A Docker pre-installed image works too, then skip step 4. | |
| 2. Point your domain to the server -> yourdomain.com | |
| 2.1. Create an A record -> @ -> server IPv4 | |
| 2.2. Create an AAAA record -> @ -> server IPv6 (optional but recommended) | |
| 2.3. Create a wildcard A record -> * -> server IPv4 if you want subdomains without touching DNS again | |
| 3. Connect over SSH -> ssh root@server-ip | |
| 4. Install Docker Engine from the official repository | |
| 4.1. sudo apt-get update | |
| 4.2. sudo apt-get install -y ca-certificates curl | |
| 4.3. sudo install -m 0755 -d /etc/apt/keyrings | |
| 4.4. sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc | |
| 4.5. sudo chmod a+r /etc/apt/keyrings/docker.asc | |
| 4.6. echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null | |
| 4.7. sudo apt-get update | |
| 4.8. sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| 4.9. sudo systemctl status docker | |
| NOTE: apt-key is deprecated and removed on modern Ubuntu. Use the keyring method above, not "curl ... | apt-key add -". | |
| 5. Check the versions | |
| 5.1. docker --version -> Docker version 29.8.0 | |
| 5.2. docker compose version -> Docker Compose version v5.5.1 | |
| NOTE: "docker compose" (plugin, v2+) replaced the old "docker-compose" python script. You do not need to install | |
| docker-compose separately any more. If a script still calls "docker-compose", install the alias package: | |
| sudo apt-get install -y docker-compose-v2 | |
| 6. Run your first container to confirm Docker works | |
| 6.1. docker run --rm ghcr.io/doxa-labs/docker-helloworld/docker-helloworld:latest | |
| 7. Create the shared proxy network. Traefik and every published container must sit on it. | |
| 7.1. docker network create proxy | |
| 7.2. docker network ls | |
| NOTE: the network name is up to you (proxy, external, traefik...). Keep it identical in every compose file below. | |
| 8. Create the Traefik folder and files | |
| 8.1. sudo mkdir -p /opt/traefik && cd /opt/traefik | |
| 8.2. sudo touch /opt/traefik/acme.json | |
| 8.3. sudo chmod 600 /opt/traefik/acme.json | |
| 8.4. sudo mkdir -p /opt/traefik/logs | |
| NOTE: acme.json MUST exist and MUST be chmod 600, otherwise Traefik refuses to start. | |
| 9. Create the dashboard password hash | |
| 9.1. sudo apt-get install -y apache2-utils | |
| 9.2. For a YAML/TOML config file (single $) -> htpasswd -nbB username 'your-password' | |
| 9.3. For a docker compose label (double $$) -> echo $(htpasswd -nbB username 'your-password') | sed -e 's/\$/\$\$/g' | |
| NOTE: this is the #1 reason basic auth "does not work". Compose interpolates $, a mounted config file does not. | |
| Doubling the $ inside a plain .yml/.toml file gives you a broken hash. | |
| ================================================================================ | |
| 1. TRAEFIK v3 (recommended) - image traefik:v3.7 | |
| ================================================================================ | |
| What changed vs v2, in one line: entrypoint level redirect + entrypoint level TLS means you write ONE router per | |
| service instead of the old "app" + "app-secure" pair, and matchers take a single value (use || to combine hosts). | |
| Files in /opt/traefik: docker-compose.yml, traefik.yml (static config), dynamic.yml (middlewares + TLS options), acme.json | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] /opt/traefik/docker-compose.yml | |
| -------------------------------------------------------------------------------- | |
| # no "version:" key - it is obsolete in Compose v2+ and prints a warning | |
| services: | |
| traefik: | |
| image: traefik:v3.7 | |
| container_name: traefik | |
| restart: unless-stopped | |
| security_opt: | |
| - no-new-privileges:true | |
| networks: | |
| - proxy | |
| ports: | |
| - "80:80" | |
| - "443:443" | |
| - "443:443/udp" # HTTP/3 (QUIC). Remove this line if you disable http3 in traefik.yml | |
| environment: | |
| - TZ=Europe/Istanbul | |
| volumes: | |
| - /etc/localtime:/etc/localtime:ro | |
| - /var/run/docker.sock:/var/run/docker.sock:ro | |
| - ./traefik.yml:/traefik.yml:ro | |
| - ./dynamic.yml:/dynamic.yml:ro | |
| - ./acme.json:/acme.json | |
| - ./logs:/var/log/traefik | |
| labels: | |
| - "traefik.enable=true" | |
| - "traefik.docker.network=proxy" | |
| - "traefik.http.routers.traefik.rule=Host(`traefik.your-domain.com`)" | |
| - "traefik.http.routers.traefik.entrypoints=websecure" | |
| - "traefik.http.routers.traefik.service=api@internal" | |
| - "traefik.http.routers.traefik.middlewares=traefik-auth@file" | |
| # TLS comes from the entrypoint default in traefik.yml, so no tls / certresolver label is needed here | |
| networks: | |
| proxy: | |
| external: true | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] /opt/traefik/traefik.yml - static config, httpChallenge | |
| -------------------------------------------------------------------------------- | |
| global: | |
| checkNewVersion: true | |
| sendAnonymousUsage: false | |
| api: | |
| dashboard: true | |
| # insecure stays false (default). The dashboard is published through the router label above. | |
| log: | |
| level: ERROR # switch to DEBUG only while troubleshooting, it is very noisy | |
| filePath: /var/log/traefik/traefik.log | |
| format: json | |
| accessLog: | |
| filePath: /var/log/traefik/access.log | |
| bufferingSize: 100 | |
| entryPoints: | |
| web: | |
| address: ":80" | |
| http: | |
| redirections: | |
| entryPoint: | |
| to: websecure | |
| scheme: https | |
| permanent: true | |
| websecure: | |
| address: ":443" | |
| http: | |
| tls: | |
| certResolver: letsencrypt | |
| http3: {} # HTTP/3 is stable in v3, no experimental flag any more | |
| providers: | |
| docker: | |
| endpoint: "unix:///var/run/docker.sock" | |
| exposedByDefault: false # a container is published only when it carries traefik.enable=true | |
| network: proxy | |
| watch: true | |
| file: | |
| filename: /dynamic.yml | |
| watch: true | |
| certificatesResolvers: | |
| letsencrypt: | |
| acme: | |
| email: your-email@your-domain.com | |
| storage: /acme.json | |
| # Uncomment while testing, then comment out and DELETE acme.json before going live. | |
| # Let's Encrypt production limits you to 5 failed attempts per hostname per hour. | |
| # caServer: https://acme-staging-v02.api.letsencrypt.org/directory | |
| httpChallenge: | |
| entryPoint: web | |
| NOTE: the http -> https redirect does NOT break the ACME http challenge. Traefik answers /.well-known/acme-challenge/ | |
| internally before the redirect is applied. | |
| NOTE: prefer tlsChallenge if port 80 is blocked by your provider - swap the httpChallenge block for: | |
| tlsChallenge: {} | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] /opt/traefik/dynamic.yml - middlewares + TLS options | |
| -------------------------------------------------------------------------------- | |
| # Single $ here. Do NOT double it, this file is not parsed by docker compose. | |
| http: | |
| middlewares: | |
| traefik-auth: | |
| basicAuth: | |
| users: | |
| - "username:$2y$05$replace-with-your-own-bcrypt-hash" | |
| secure-headers: | |
| headers: | |
| forceSTSHeader: true | |
| stsSeconds: 63072000 | |
| stsIncludeSubdomains: true | |
| stsPreload: true | |
| frameDeny: true | |
| contentTypeNosniff: true | |
| referrerPolicy: strict-origin-when-cross-origin | |
| customRequestHeaders: | |
| X-Forwarded-Proto: https | |
| # v2 name was ipWhiteList, renamed in v3 | |
| office-only: | |
| ipAllowList: | |
| sourceRange: | |
| - "1.2.3.4/32" | |
| tls: | |
| options: | |
| default: | |
| minVersion: VersionTLS12 | |
| curvePreferences: | |
| - X25519 | |
| - CurveP256 | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] start it | |
| -------------------------------------------------------------------------------- | |
| 1. cd /opt/traefik | |
| 2. docker compose up -d | |
| 3. docker ps | |
| 4. docker compose logs -f traefik # certificate errors show up here | |
| 5. Create a CNAME (or A record) for traefik -> visit https://traefik.your-domain.com | |
| 6. Log in with the username and the PLAIN password (not the hash) | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] /opt/your-app/docker-compose.yml - publishing your own container | |
| -------------------------------------------------------------------------------- | |
| services: | |
| your-app: | |
| image: ghcr.io/github-username/repo-name:latest | |
| container_name: your-app | |
| restart: unless-stopped | |
| security_opt: | |
| - no-new-privileges:true | |
| networks: | |
| - proxy | |
| environment: | |
| - TZ=Europe/Istanbul | |
| volumes: | |
| - /etc/localtime:/etc/localtime:ro | |
| - ./data:/data | |
| labels: | |
| - "traefik.enable=true" | |
| - "traefik.docker.network=proxy" | |
| - "traefik.http.routers.your-app.rule=Host(`your-app.your-domain.com`) || Host(`www.your-app.your-domain.com`)" | |
| - "traefik.http.routers.your-app.entrypoints=websecure" | |
| - "traefik.http.routers.your-app.middlewares=secure-headers@file" | |
| - "traefik.http.services.your-app.loadbalancer.server.port=80" # the port INSIDE the container | |
| networks: | |
| proxy: | |
| external: true | |
| NOTE: v3 matchers take exactly one value. Host(`a.com`, `b.com`) was valid in v2 and is rejected in v3. | |
| Write Host(`a.com`) || Host(`b.com`). | |
| NOTE: your app container does not publish any port to the host. Traefik reaches it over the proxy network. | |
| -------------------------------------------------------------------------------- | |
| [Traefik v3] dnsChallenge - wildcard certificate for *.your-domain.com | |
| -------------------------------------------------------------------------------- | |
| Use this when you want one certificate covering every subdomain, or when ports 80/443 are not publicly reachable. | |
| traefik.yml: | |
| certificatesResolvers: | |
| letsencrypt-dns: | |
| acme: | |
| email: your-email@your-domain.com | |
| storage: /acme.json | |
| dnsChallenge: | |
| provider: cloudflare # godaddy, digitalocean, route53, ovh... see the provider list in the docs | |
| delayBeforeCheck: 0 | |
| resolvers: | |
| - "1.1.1.1:53" | |
| - "8.8.8.8:53" | |
| docker-compose.yml of traefik, environment section: | |
| environment: | |
| - TZ=Europe/Istanbul | |
| - CF_DNS_API_TOKEN=... # Cloudflare | |
| # - GODADDY_API_KEY=... # GoDaddy | |
| # - GODADDY_API_SECRET=... | |
| Labels on the container that should get the wildcard: | |
| - "traefik.http.routers.your-app.tls.certresolver=letsencrypt-dns" | |
| - "traefik.http.routers.your-app.tls.domains[0].main=your-domain.com" | |
| - "traefik.http.routers.your-app.tls.domains[0].sans=*.your-domain.com" | |
| NOTE: GoDaddy restricted its production DNS API to accounts holding 10+ domains, so the v1 recipe further down may | |
| no longer work on a small account. Cloudflare DNS is the easiest free replacement. | |
| NOTE: keep secrets out of the compose file with an .env file next to it and "env_file: .env". | |
| ================================================================================ | |
| 2. TRAEFIK v2 (legacy) - image traefik:v2.11 | |
| ================================================================================ | |
| Only for servers that already run v2. v2 gets security fixes, nothing else. v2.11 is the last minor of the v2 line. | |
| -------------------------------------------------------------------------------- | |
| [Traefik v2.11] /opt/traefik/docker-compose.yml with httpChallenge | |
| -------------------------------------------------------------------------------- | |
| services: | |
| traefik: | |
| image: traefik:v2.11 | |
| container_name: traefik | |
| restart: unless-stopped | |
| security_opt: | |
| - no-new-privileges:true | |
| networks: | |
| - proxy | |
| ports: | |
| - "80:80" | |
| - "443:443" | |
| volumes: | |
| - /etc/localtime:/etc/localtime:ro | |
| - /var/run/docker.sock:/var/run/docker.sock:ro | |
| - ./traefik.yml:/traefik.yml:ro | |
| - ./acme.json:/acme.json | |
| labels: | |
| - "traefik.enable=true" | |
| - "traefik.http.routers.traefik.entrypoints=http" | |
| - "traefik.http.routers.traefik.rule=Host(`traefik.your-domain.com`)" | |
| - "traefik.http.middlewares.traefik-auth.basicauth.users=username:hashed-password" | |
| - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https" | |
| - "traefik.http.routers.traefik.middlewares=traefik-https-redirect" | |
| - "traefik.http.routers.traefik-secure.entrypoints=https" | |
| - "traefik.http.routers.traefik-secure.rule=Host(`traefik.your-domain.com`)" | |
| - "traefik.http.routers.traefik-secure.middlewares=traefik-auth" | |
| - "traefik.http.routers.traefik-secure.tls=true" | |
| - "traefik.http.routers.traefik-secure.tls.certresolver=http" | |
| - "traefik.http.routers.traefik-secure.service=api@internal" | |
| - "traefik.http.services.traefik.loadbalancer.server.port=8080" | |
| networks: | |
| proxy: | |
| external: true | |
| NOTE: the hashed-password in a LABEL needs every $ doubled to $$. See step 9.3. | |
| -------------------------------------------------------------------------------- | |
| [Traefik v2.11] /opt/traefik/traefik.yml with httpChallenge | |
| -------------------------------------------------------------------------------- | |
| api: | |
| dashboard: true | |
| log: | |
| level: ERROR | |
| filePath: "log-file.log" | |
| format: json | |
| accessLog: | |
| filePath: "log-access.log" | |
| bufferingSize: 100 | |
| entryPoints: | |
| http: | |
| address: ":80" | |
| https: | |
| address: ":443" | |
| providers: | |
| docker: | |
| endpoint: "unix:///var/run/docker.sock" | |
| exposedByDefault: false | |
| certificatesResolvers: | |
| http: | |
| acme: | |
| email: your-email@your-domain.com | |
| storage: acme.json | |
| httpChallenge: | |
| entryPoint: http | |
| -------------------------------------------------------------------------------- | |
| [Traefik v2.11] your-container docker-compose.yml | |
| -------------------------------------------------------------------------------- | |
| services: | |
| your-container-name: | |
| image: ghcr.io/github-username/repo-name:latest | |
| container_name: your-container-name | |
| restart: unless-stopped | |
| security_opt: | |
| - no-new-privileges:true | |
| networks: | |
| - proxy | |
| volumes: | |
| - /etc/localtime:/etc/localtime:ro | |
| - ./data:/data | |
| labels: | |
| - "traefik.enable=true" | |
| - "traefik.docker.network=proxy" | |
| - "traefik.http.routers.your-container-name.entrypoints=http" | |
| - "traefik.http.routers.your-container-name.rule=Host(`your-container-name.your-domain.com`, `www.your-container-name.your-domain.com`)" | |
| - "traefik.http.middlewares.your-container-name-https-redirect.redirectscheme.scheme=https" | |
| - "traefik.http.routers.your-container-name.middlewares=your-container-name-https-redirect" | |
| - "traefik.http.routers.your-container-name-secure.entrypoints=https" | |
| - "traefik.http.routers.your-container-name-secure.rule=Host(`your-container-name.your-domain.com`, `www.your-container-name.your-domain.com`)" | |
| - "traefik.http.routers.your-container-name-secure.tls=true" | |
| - "traefik.http.routers.your-container-name-secure.tls.certresolver=http" | |
| - "traefik.http.routers.your-container-name-secure.service=your-container-name" | |
| - "traefik.http.services.your-container-name.loadbalancer.server.port=80" | |
| networks: | |
| proxy: | |
| external: true | |
| NOTE: mounting /var/run/docker.sock into an application container (the old version of this gist did) gives that app | |
| root on the host. Only Traefik needs the socket. | |
| ================================================================================ | |
| 3. TRAEFIK v1 (EOL) - image traefik:v1.7 | |
| ================================================================================ | |
| v1.7 reached end of life in 2019 and receives no fixes. Kept for reference on old servers only. | |
| Config format is TOML, labels use the old traefik.frontend.* / traefik.backend.* scheme. | |
| -------------------------------------------------------------------------------- | |
| [Traefik v1.7] docker-compose.yml with dnsChallenge | |
| -------------------------------------------------------------------------------- | |
| version: '3.7' | |
| services: | |
| reverse-proxy: | |
| image: traefik:v1.7 | |
| restart: always | |
| container_name: traefik | |
| ports: | |
| - 80:80 | |
| - 443:443 | |
| expose: | |
| - 8080 | |
| networks: | |
| - external | |
| - internal | |
| environment: | |
| - GODADDY_API_KEY=... | |
| - GODADDY_API_SECRET=... | |
| volumes: | |
| - /var/run/docker.sock:/var/run/docker.sock:ro | |
| - /opt/traefik/traefik.toml:/traefik.toml | |
| - /opt/traefik/acme.json:/acme.json | |
| labels: | |
| - "traefik.backend=traefik" | |
| - "traefik.docker.network=external" | |
| - "traefik.enable=true" | |
| - "traefik.frontend.rule=Host:traefik.yourdomain.com" | |
| - "traefik.port=8080" | |
| - "traefik.frontend.headers.forceSTSHeader=true" | |
| - "traefik.frontend.headers.STSSeconds=315360000" | |
| - "traefik.frontend.headers.STSIncludeSubdomains=true" | |
| - "traefik.frontend.headers.STSPreload=true" | |
| networks: | |
| external: | |
| external: true | |
| internal: | |
| -------------------------------------------------------------------------------- | |
| [Traefik v1.7] traefik.toml with dnsChallenge | |
| -------------------------------------------------------------------------------- | |
| Update users on entryPoints.traefik with -> echo $(htpasswd -nb username password) | sed -e s/\\$/\\$\\$/g | |
| Update your-domain and your-email fields, and the provider on acme.dnsChallenge. | |
| debug = false | |
| loglevel = "ERROR" | |
| defaultEntryPoints = ["https", "http"] | |
| [entryPoints] | |
| [entryPoints.http] | |
| address = ":80" | |
| [entryPoints.http.redirect] | |
| entryPoint = "https" | |
| [entryPoints.https] | |
| address = ":443" | |
| [entryPoints.https.tls] | |
| [entryPoints.traefik] | |
| address = ":8080" | |
| [entryPoints.traefik.auth.basic] | |
| users = ["username:hashed-password"] | |
| [api] | |
| entryPoint = "traefik" | |
| [retry] | |
| [docker] | |
| endpoint = "unix:///var/run/docker.sock" | |
| domain = "your-domain.com" | |
| watch = true | |
| exposedByDefault = false | |
| [acme] | |
| email = "your-email@your-domain.com" | |
| storage = "acme.json" | |
| onHostRule = true | |
| entryPoint = "https" | |
| [acme.dnsChallenge] | |
| provider = "godaddy" | |
| delayBeforeCheck = 0 | |
| [[acme.domains]] | |
| main = "*.your-domain.com" | |
| sans = ["your-domain.com"] | |
| -------------------------------------------------------------------------------- | |
| [Traefik v1.7] your-container docker-compose.yml | |
| -------------------------------------------------------------------------------- | |
| version: "3.9" | |
| services: | |
| your-container-name: | |
| image: ghcr.io/github-username/repo-name:latest | |
| restart: always | |
| container_name: your-container-name | |
| networks: | |
| - web | |
| labels: | |
| - traefik.docker.network=external | |
| - traefik.backend=your-container-name | |
| - traefik.frontend.rule=Host:your-container-name.your-domain.com | |
| - traefik.enable=true | |
| - traefik.port=80 | |
| networks: | |
| web: | |
| name: external | |
| ================================================================================ | |
| 4. GHCR.IO LOGIN, FIRST CONTAINER, TROUBLESHOOTING | |
| ================================================================================ | |
| 1. Log in to GitHub Container Registry | |
| 1.1. Create a GitHub Personal Access Token (classic) with read:packages (and write:packages if you push from CI) | |
| 1.2. export CR_PAT=ghp_yourtoken | |
| 1.3. echo $CR_PAT | docker login ghcr.io -u github-username --password-stdin | |
| 1.4. The token is stored in plain text in ~/.docker/config.json. On a shared server use a credential helper. | |
| NOTE: docker.pkg.github.com (GitHub Packages Docker registry) is retired. Image names are now | |
| ghcr.io/OWNER/IMAGE:TAG - all lowercase. | |
| NOTE: a public package needs no login at all on the server, only for pushing. | |
| 2. Run your container | |
| 2.1. sudo mkdir -p /opt/your-app && cd /opt/your-app | |
| 2.2. nano docker-compose.yml (paste the app compose from the section of your Traefik version) | |
| 2.3. docker compose up -d | |
| 2.4. docker ps | |
| 2.5. Create a DNS record for your-app -> visit https://your-app.your-domain.com | |
| 3. Update a container to the latest image | |
| 3.1. docker compose pull | |
| 3.2. docker compose up -d | |
| 3.3. docker image prune -f | |
| 4. Troubleshooting | |
| - 404 page not found -> the router did not match. Check the Host rule, and that the container really is | |
| on the proxy network: docker inspect -f '{{json .NetworkSettings.Networks}}' your-app | |
| - Bad Gateway -> Traefik found the router but not the app. loadbalancer.server.port must be the port | |
| INSIDE the container, not a published host port. | |
| - TRAEFIK DEFAULT CERT -> no certificate was issued. docker compose logs traefik | grep -i acme | |
| - unable to generate a certificate -> DNS not pointing at this server yet, or port 80 is firewalled, or you hit the | |
| Let's Encrypt rate limit (5 failures per hostname per hour). Use the staging | |
| caServer while you debug. | |
| - basic auth loop / 401 -> the $ escaping. Single $ in a mounted yml file, double $$ in a compose label. | |
| - acme.json permissions -> must be chmod 600, owned by root. | |
| - port 80 already in use -> another nginx/apache is running: sudo systemctl disable --now nginx apache2 | |
| - firewall -> sudo ufw allow 80,443/tcp && sudo ufw allow 443/udp (443/udp only for HTTP/3) | |
| - validate the static config -> docker run --rm -v /opt/traefik/traefik.yml:/traefik.yml traefik:v3.7 version | |
| ================================================================================ | |
| 5. v2 -> v3 MIGRATION CHECKLIST | |
| ================================================================================ | |
| 1. Router rules: a matcher now takes a single value. | |
| v2: Host(`a.com`, `b.com`) v3: Host(`a.com`) || Host(`b.com`) | |
| 2. Renamed matchers: Headers -> Header, HeadersRegexp -> HeaderRegexp, HostHeader removed (use Host). | |
| 3. PathPrefix no longer accepts a regular expression, and {name} placeholders are gone. Use PathRegexp. | |
| 4. Middleware renamed: ipWhiteList -> ipAllowList (label: traefik.http.middlewares.x.ipallowlist.sourcerange). | |
| 5. Removed Headers middleware options: sslRedirect, sslTemporaryRedirect, sslHost, sslForceHost, featurePolicy. | |
| Use a redirectScheme middleware or the entrypoint redirection instead of sslRedirect. | |
| 6. StripPrefix lost forceSlash. | |
| 7. Docker provider was split: swarmMode is gone, Docker Swarm now uses the separate "swarm" provider. | |
| 8. experimental.http3 is gone - HTTP/3 is configured per entrypoint with "http3: {}" and needs 443/udp published. | |
| 9. Removed providers: Marathon, Rancher v1, and the Pilot service. tracing.datadog.globaltag removed. | |
| 10. Simplify while you are there: move the http->https redirect and the certResolver onto the entrypoints, then delete | |
| every "-secure" duplicate router and its redirect middleware. | |
| 11. Escape hatch if you cannot rewrite all the rules at once - keep the v2 rule syntax in the static config: | |
| core: | |
| defaultRuleSyntax: v2 | |
| Then move to v3 syntax router by router with the label | |
| traefik.http.routers.your-app.rulesyntax=v3 | |
| 12. Test with the Let's Encrypt staging caServer first so a broken migration does not burn your rate limit. | |
| References: | |
| - Traefik v3 docs -> https://doc.traefik.io/traefik/ | |
| - v2 to v3 migration -> https://doc.traefik.io/traefik/migrate/v2-to-v3/ | |
| - Let's Encrypt limits -> https://letsencrypt.org/docs/rate-limits/ | |
| - Docker install docs -> https://docs.docker.com/engine/install/ubuntu/ |
Author
Can't proxy to container though. I already setup subdomain mycontainer.mydomain.com point to IP of server.
Author
I've updated the Gist for Docker 29 + Traefik v3.7, v2.11 and v1.7 + Let's Encrypt + Github Registry V2 ghcr.io on 4 September 2026.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've updated the Gist for Docker 23 + Traefik v2.9.9 and v1.7 + Let's Encrypt + Github Registry V2 ghcr.io on 28 March 2023.