diff --git a/README.md b/README.md index 82f532d..7bec6ad 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,64 @@ # swarm-certbot-traefik -[Traefik Proxy](https://doc.traefik.io/traefik/v2.11/) community edition does not really support Let's Encrypt in a serious way for **docker swarm**. If you have multiple instances of traefik with [letsencrypt](https://doc.traefik.io/traefik/https/acme/) support enabled, they would all start to generate same certificates, overwriting `acme.json` storage and exhausting the limits very quickly. +[Traefik Proxy](https://doc.traefik.io/traefik/v2.11/) community edition does not really support Let's Encrypt in a serious way for **docker swarm**. If you have multiple instances of traefik with [letsencrypt](https://doc.traefik.io/traefik/https/acme/) support enabled, they would all start to generate same certificates, overwriting `acme.json` storage and exhausting the limits very quickly if things go wrong. -## Functionality +This project handles certificates using separate service (here called `certbot`), which exports file with certificates in format expected by `traefik`. It uses auto-discovery by searching for `certbot.domain` labels. Please check following examples, which show both `traefik` and `certbot` labels: + +```yml + vector: + image: timberio/vector:0.36.1-alpine + networks: + - web + deploy: + labels: + - "traefik.enable=true" + - "traefik.docker.network=web" + - "traefik.http.routers.myproject-vector.entrypoints=websecure" + - "traefik.http.routers.myproject-vector.rule=Host(`sink.example.com`)" + - "traefik.http.services.myproject-vector.loadbalancer.server.port=8687" + - "certbot.domain=sink.example.com" +``` -The provided example of `manager-stack.yml` provides following functionality: +## Example stacks -- Traefik dashboard protected by basic authentication - replace `REPLACE-ME-USE` with your password generated by `htpasswd -nb`. -- Dashboard itself is covered by created certificate - replace `traefik.example.com` with your dashboard domain. -- Generic redirect from 80 to 433 with exception of ACME challenge. +This project shows two examples of docker swarm stacks: +- [manager_single_stack.yml](examples/manager_single_stack.yml) - for single traefik instance, +- [manager_multiple_stack.yml](examples/manager_multiple_stack.yml) - with two main traefik instances (to support rolling update) running on the same host, in this case there is an `edge` traefic instance added which acts as a TCP proxy, in this setup, it is expected, that there is only one public IP without load balancing. + +## Functionality + +- Traefik dashboard is protected by basic authentication. +- Generic redirect from 80 to 433 is provided (with exception of ACME challenge). - Dynamic loading of generated certificates - Treafik actually requires TLS to be in a dynamically loaded file. -- Challenge webroot gets automatically routed by traefik, but only gets opened when needed. -- Automatic discovery of domains, that require cerificate - use `certbot.domain` label - you can separate multiple domains with commas. +- Challenge requests get automatically routed by traefik - the server serving webroot directory is only started when needed. - Renewal is performed once in a day, when date change is detected. You can force from outside using: ```sh SERVICE_NAME=manager_certbot; docker exec --tty $(docker ps --format json | jq -r 'select(.Names | startswith("'$SERVICE_NAME'")) | .ID') ./renew.sh ``` -> [!IMPORTANT] -> - When certbot fails to generate certificate it would store log into `/etc/letsencrypt/failed/$DOMAIN` - you have to delete it to get another attempt. -> - The setup expects that multiple copies of traefik run on the same node (manager) to handle rolling update of traefik service. +## Configuration +Use `certbot.domain` label - you can separate multiple domains with commas. No attempt to reuse traefik labels was done, there might be scenarios where traefik uses wildcards, but certbot needs to know the names: ```yml -version: '3.8' - -services: - - traefik: - image: traefik:v3.0 - tty: true - networks: - - web - ports: - - "80:80" - - "443:443" - deploy: - replicas: 2 - placement: - constraints: - - node.role == manager - update_config: - delay: 10s - labels: + labels: - "traefik.enable=true" - "traefik.docker.network=web" - - "traefik.http.middlewares.auth.basicauth.users=admin:REPLACE-ME-USE" - - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - - "traefik.http.routers.dashboard.entrypoints=websecure" - - "traefik.http.routers.dashboard.middlewares=auth" - - "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)" - - "traefik.http.routers.dashboard.service=api@internal" - - "traefik.http.routers.http-catchall.entrypoints=web" - - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" - - "traefik.http.routers.http-catchall.rule=HostRegexp(`.*`)" - - "traefik.http.routers.http-catchall.priority=1000" - - "traefik.http.services.dashboard.loadbalancer.server.port=8080" - - "certbot.domain=traefik.example.com" - command: - - "--log.level=DEBUG" - - "--api.dashboard=true" - - "--providers.swarm.endpoint=unix:///var/run/docker.sock" - - "--providers.swarm.exposedByDefault=false" - - "--entrypoints.web.address=:80" - - "--entrypoints.websecure.address=:443" - - "--entrypoints.websecure.http.tls=true" - - "--accesslog=true" - - "--accesslog.format=json" - - "--providers.file.filename=/etc/letsencrypt/traefik.yml" - - "--providers.file.watch=true" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /etc/letsencrypt:/etc/letsencrypt + - "traefik.http.routers.myproject-nginx.entrypoints=websecure" + - "traefik.http.routers.myproject-nginx.rule=HostRegexp(`(.*).example.com`)" + - "traefik.http.services.myproject-nginx.loadbalancer.server.port=80" + - "certbot.domain=admin.example.com,www.example.com" +``` - certbot: - image: brablc/swarm-certbot-traefik - tty: true - networks: - - web - deploy: - replicas: 1 - placement: - constraints: - - node.role == manager - labels: - - "traefik.enable=true" - - "traefik.docker.network=web" - - "traefik.http.routers.certbot.entrypoints=web" - - "traefik.http.routers.certbot.rule=PathPrefix(`/.well-known/acme-challenge`)" - - "traefik.http.routers.certbot.priority=1010" - - "traefik.http.services.certbot.loadbalancer.server.port=80" - environment: - LOOP_SLEEP: 60s - CERTBOT_EMAIL: hostmaster@example.com - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /etc/letsencrypt:/etc/letsencrypt +When using provided stacks please change following: -networks: - web: - external: true -``` +- Replace `REPLACE-PASSWORD` with your password generated by `htpasswd -nb`. +- Dashboard itself is covered by created certificate - replace `traefik.example.com` with your dashboard domain. +- Replace `hostmaster@example.com` environment variable with your email for Let's Encrypt registration. + +> [!IMPORTANT] +> - When certbot fails to generate certificate it would store log into `/etc/letsencrypt/failed/$DOMAIN` - you have to delete it to get another attempt. +> - The setup expects that multiple copies of traefik run on the same node (manager) to handle rolling update of traefik service. ## Credits