From 4fd0429cd3a11f48f2fafea57a882c7416860a0c Mon Sep 17 00:00:00 2001 From: j4n Date: Mon, 16 Feb 2026 20:21:08 +0100 Subject: [PATCH] docker: add Traefik support USE_FOREIGN_CERT_MANAGER existed in compose/example.env but was never read by any code. This wires it up end-to-end based on PR 662. - Preliminarily add config options for this, and skip AcmetoolDeployer if set. - Add Traefik integration in docker/docker-compose-traefik.yaml, with traefik-certs-dumper - post-hook.sh creates fullchain/privkey symlinks for chatmail - Chatmail container uses ports 25/143/465/587/993 directly, Traefik handles 80/443 - docker/traefik/ contains config.yaml and dynamic configs - docker/example-traefik.env for the Traefik setup - rename USE_FOREIGN_CERT_MANAGER to CHATMAIL_NOACME --- chatmaild/src/chatmaild/config.py | 1 + chatmaild/src/chatmaild/ini/chatmail.ini.f | 1 + cmdeploy/src/cmdeploy/deployers.py | 7 +- docker-compose.yaml | 2 +- docker/docker-compose-traefik.yaml | 116 +++++++++++++++++++ docker/example-traefik.env | 5 + docker/example.env | 2 +- docker/files/entrypoint.sh | 2 +- docker/traefik/config.yaml | 30 +++++ docker/traefik/dynamic-configs/insecure.yaml | 4 + docker/traefik/post-hook.sh | 12 ++ 11 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 docker/docker-compose-traefik.yaml create mode 100644 docker/example-traefik.env create mode 100644 docker/traefik/config.yaml create mode 100644 docker/traefik/dynamic-configs/insecure.yaml create mode 100644 docker/traefik/post-hook.sh diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index d19e966a..a10cdc43 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -44,6 +44,7 @@ class Config: ) self.mtail_address = params.get("mtail_address") self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true" + self.noacme = os.environ.get("CHATMAIL_NOACME", "").lower() == "true" self.addr_v4 = os.environ.get("CHATMAIL_ADDR_V4", "") self.addr_v6 = os.environ.get("CHATMAIL_ADDR_V6", "") self.acme_email = params.get("acme_email", "") diff --git a/chatmaild/src/chatmaild/ini/chatmail.ini.f b/chatmaild/src/chatmaild/ini/chatmail.ini.f index 29d7baa9..5036a07e 100644 --- a/chatmaild/src/chatmaild/ini/chatmail.ini.f +++ b/chatmaild/src/chatmaild/ini/chatmail.ini.f @@ -66,6 +66,7 @@ postfix_reinject_port_incoming = 10026 # if set to "True" IPv6 is disabled disable_ipv6 = False + # Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates acme_email = diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index 4bf2995f..071d48c6 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -610,7 +610,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) - UnboundDeployer(config), TurnDeployer(mail_domain), IrohDeployer(config.enable_iroh_relay), - AcmetoolDeployer(config.acme_email, tls_domains), + ] + + if not config.noacme: + all_deployers.append(AcmetoolDeployer(config.acme_email, tls_domains)) + + all_deployers += [ WebsiteDeployer(config), ChatmailVenvDeployer(config), MtastsDeployer(), diff --git a/docker-compose.yaml b/docker-compose.yaml index 0c9ad92e..c3fb6a4b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,7 +29,7 @@ services: environment: MAIL_DOMAIN: $MAIL_DOMAIN CMDEPLOY_STAGES: ${CMDEPLOY_STAGES:-} - USE_FOREIGN_CERT_MANAGER: ${USE_FOREIGN_CERT_MANAGER:-} + CHATMAIL_NOACME: ${CHATMAIL_NOACME:-} network_mode: "host" volumes: ## system diff --git a/docker/docker-compose-traefik.yaml b/docker/docker-compose-traefik.yaml new file mode 100644 index 00000000..5c427c37 --- /dev/null +++ b/docker/docker-compose-traefik.yaml @@ -0,0 +1,116 @@ +# Traefik reverse proxy + cert manager for chatmail. +# Use this instead of docker-compose.yaml when Traefik manages TLS certificates. +# +# Required .env vars: +# MAIL_DOMAIN=chat.example.com +# ACME_EMAIL=admin@example.com +# +# Usage: +# cp docker/example-traefik.env .env +# docker compose -f docker/docker-compose-traefik.yaml build +# docker compose -f docker/docker-compose-traefik.yaml up -d + +services: + chatmail: + build: + context: ../ + dockerfile: docker/chatmail_relay.dockerfile + image: chatmail-relay:latest + restart: unless-stopped + container_name: chatmail + depends_on: + traefik-certs-dumper: + condition: service_started + cgroup: host + tty: true + tmpfs: + - /tmp + - /run + - /run/lock + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + environment: + MAIL_DOMAIN: $MAIL_DOMAIN + CMDEPLOY_STAGES: ${CMDEPLOY_STAGES:-} + CHATMAIL_NOACME: "true" + PATH_TO_SSL: /var/lib/acme/live/${MAIL_DOMAIN} + ports: + - "25:25" + - "143:143" + - "465:465" + - "587:587" + - "993:993" + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + - chatmail-data:/home + - chatmail-dkimkeys:/etc/dkimkeys + - traefik-certs:/var/lib/acme/live:ro + + labels: + - traefik.enable=true + - traefik.http.services.chatmail.loadbalancer.server.scheme=https + - traefik.http.services.chatmail.loadbalancer.server.port=443 + - traefik.http.services.chatmail.loadbalancer.serverstransport=insecure@file + - traefik.http.routers.chatmail.rule=Host(`${MAIL_DOMAIN}`) || Host(`mta-sts.${MAIL_DOMAIN}`) || Host(`www.${MAIL_DOMAIN}`) + - traefik.http.routers.chatmail.tls=true + - traefik.http.routers.chatmail.tls.certresolver=letsEncrypt + + traefik: + image: traefik:v3.3 + container_name: traefik + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + command: + - "--configFile=/config.yaml" + - "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL}" + network_mode: host + depends_on: + traefik-init: + condition: service_completed_successfully + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/config.yaml:/config.yaml:ro + - traefik-data:/data + - ./traefik/dynamic-configs:/dynamic/conf:ro + + traefik-init: + image: alpine:latest + restart: "no" + entrypoint: sh -c 'touch /data/acme.json && chmod 600 /data/acme.json' + volumes: + - traefik-data:/data + + traefik-certs-dumper: + image: ldez/traefik-certs-dumper:v2.10.0 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + depends_on: + - traefik + entrypoint: sh -c ' + apk add openssl + && while ! [ -e /data/acme.json ] || ! [ "$$(jq ".[] | .Certificates | length" /data/acme.json | jq -s "add")" != "0" ]; do + sleep 1 + ; done + && traefik-certs-dumper file --version v3 --watch --domain-subdir=true + --source /data/acme.json --dest /certs --post-hook "sh /post-hook.sh"' + volumes: + - traefik-data:/data:ro + - traefik-certs:/certs + - ./traefik/post-hook.sh:/post-hook.sh:ro + +volumes: + chatmail-data: + chatmail-dkimkeys: + traefik-data: + traefik-certs: diff --git a/docker/example-traefik.env b/docker/example-traefik.env new file mode 100644 index 00000000..925f27e8 --- /dev/null +++ b/docker/example-traefik.env @@ -0,0 +1,5 @@ +MAIL_DOMAIN="chat.example.com" +ACME_EMAIL="admin@example.com" + +# CMDEPLOY_STAGES - default: "configure,activate". Set to "install,configure,activate" to force full reinstall. +# CMDEPLOY_STAGES="configure,activate" diff --git a/docker/example.env b/docker/example.env index da73f8bd..ba62c93c 100644 --- a/docker/example.env +++ b/docker/example.env @@ -4,4 +4,4 @@ MAIL_DOMAIN="chat.example.com" # CMDEPLOY_STAGES="configure,activate" # Skip acmetool when using an external certificate manager (e.g. Traefik, Caddy). -# USE_FOREIGN_CERT_MANAGER="True" +# CHATMAIL_NOACME="True" diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh index 3f72bfdb..f6a3fb1f 100755 --- a/docker/files/entrypoint.sh +++ b/docker/files/entrypoint.sh @@ -6,7 +6,7 @@ SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/ # Whitelist only the env vars needed by setup_chatmail_docker.sh. # Forwarding all env vars (via printenv) would leak Docker internals, # orchestrator secrets, and other unrelated variables into systemd. -env_vars="MAIL_DOMAIN CMDEPLOY_STAGES CHATMAIL_INI CHATMAIL_NOSYSCTL CHATMAIL_NOPORTCHECK PATH_TO_SSL PATH USE_FOREIGN_CERT_MANAGER" +env_vars="MAIL_DOMAIN CMDEPLOY_STAGES CHATMAIL_INI CHATMAIL_NOSYSCTL CHATMAIL_NOPORTCHECK CHATMAIL_NOACME PATH_TO_SSL PATH" sed -i "s||$env_vars|g" "$SETUP_CHATMAIL_SERVICE_PATH" exec /lib/systemd/systemd "$@" diff --git a/docker/traefik/config.yaml b/docker/traefik/config.yaml new file mode 100644 index 00000000..cc759d4d --- /dev/null +++ b/docker/traefik/config.yaml @@ -0,0 +1,30 @@ +log: + level: INFO + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + permanent: true + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + file: + directory: /dynamic/conf + watch: true + +certificatesResolvers: + letsEncrypt: + acme: + storage: /data/acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + tlschallenge: true + httpChallenge: + entryPoint: web diff --git a/docker/traefik/dynamic-configs/insecure.yaml b/docker/traefik/dynamic-configs/insecure.yaml new file mode 100644 index 00000000..acafed2e --- /dev/null +++ b/docker/traefik/dynamic-configs/insecure.yaml @@ -0,0 +1,4 @@ +http: + serversTransports: + insecure: + insecureSkipVerify: true diff --git a/docker/traefik/post-hook.sh b/docker/traefik/post-hook.sh new file mode 100644 index 00000000..879fb6db --- /dev/null +++ b/docker/traefik/post-hook.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Post-hook for traefik-certs-dumper: create symlinks from Traefik's +# cert dump format to the paths chatmail expects (fullchain, privkey). +CERTS_DIR="${CERTS_DIR:-/certs}" + +for dir in "$CERTS_DIR"/*/; do + [ -d "$dir" ] || continue + cd "$dir" + [ -f "certificate.crt" ] && ln -sf certificate.crt fullchain + [ -f "privatekey.key" ] && ln -sf privatekey.key privkey + cd - > /dev/null +done