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
This commit is contained in:
j4n
2026-02-16 20:21:08 +01:00
parent 45717de6cb
commit 4fd0429cd3
11 changed files with 178 additions and 4 deletions

View File

@@ -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", "")

View File

@@ -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 =

View File

@@ -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(),

View File

@@ -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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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"

View File

@@ -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|<envs_list>|$env_vars|g" "$SETUP_CHATMAIL_SERVICE_PATH"
exec /lib/systemd/systemd "$@"

View File

@@ -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

View File

@@ -0,0 +1,4 @@
http:
serversTransports:
insecure:
insecureSkipVerify: true

View File

@@ -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