One-shot Traefik v3 ACME dump image with an optional Mailcow copy hook. https://git.openso.de/openso/traefik-certs-dumper
  • Shell 69.8%
  • Python 27.6%
  • Dockerfile 2.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Marcel Henneke 4e607596e9
All checks were successful
ci / compose (push) Successful in 12s
ci / shellcheck (push) Successful in 2s
ci / conventions (push) Successful in 2s
ci / image (push) Successful in 35s
docs(readme): use short-form volume syntax in certs-dumper example (#4)
2026-11-01 14:42:17 +01:00
.forgejo Publish the image with the package token instead of the job token. 2026-10-27 15:51:48 +01:00
.githooks Add the certs-dumper image so the first push to main builds and publishes it. 2026-10-27 14:48:39 +01:00
.openso Move Mailcow copy hook into image behind MAILCOW_ENABLED (#3) 2026-11-01 12:45:02 +01:00
.dockerignore Move Mailcow copy hook into image behind MAILCOW_ENABLED (#3) 2026-11-01 12:45:02 +01:00
.gitignore Add the certs-dumper image so the first push to main builds and publishes it. 2026-10-27 14:48:39 +01:00
AGENTS.md Replace local rule tree with docker-templates reference in AGENTS.md (#1) 2026-10-27 17:02:44 +01:00
Dockerfile Move Mailcow copy hook into image behind MAILCOW_ENABLED (#3) 2026-11-01 12:45:02 +01:00
entrypoint.traefik-certs-dumper.sh Move Mailcow copy hook into image behind MAILCOW_ENABLED (#3) 2026-11-01 12:45:02 +01:00
LICENSE Add the certs-dumper image so the first push to main builds and publishes it. 2026-10-27 14:48:39 +01:00
mailcow-hook.sh Move Mailcow copy hook into image behind MAILCOW_ENABLED (#3) 2026-11-01 12:45:02 +01:00
README.md docs(readme): use short-form volume syntax in certs-dumper example (#4) 2026-11-01 14:42:17 +01:00

traefik-certs-dumper

One-shot Traefik v3 ACME dump image. The entrypoint snapshots acme.json and writes PEM files. With MAILCOW_ENABLED=true it then copies the SMTP certificate to Mailcow over SSH, updates the existing TLSA record, and restarts postfix, dovecot, and nginx.

The customer stack lives in docker-templates at templates/traefik_certs-dumper. This repository is the image only.

Image

git.openso.de/openso/traefik-certs-dumper:main

docker-templates uses git.openso.de/openso/traefik-certs-dumper with no tag, so Compose pulls :latest. :latest tracks the same build as :main.

The process runs as uid/gid 1000 (certsdumper). The root filesystem is expected to be read-only. Writable paths are tmpfs plus the PEM destination.

Runtime

On start the entrypoint:

  1. Requires ACME_FILENAME (one basename, characters A-Za-z0-9._-).
  2. Waits until ${ACME_DIR}/${ACME_FILENAME} exists and its JSON lists at least one certificate.
  3. Polls forever. Each cycle copies the store to a tmpfs snapshot, skips the cycle when the snapshot digest is unchanged or another dump holds the lock, then runs traefik-certs-dumper file with --version v3, --domain-subdir, --crt-ext=.pem, and --key-ext=.pem.
  4. When MAILCOW_ENABLED=true, runs /usr/local/bin/mailcow-hook.sh under the same lock. The entrypoint exports CERTS_DUMPER_SUPERVISOR_LOCK=1 for that run. false skips the hook.
  5. After a successful dump, and a successful hook when Mailcow is enabled, writes ready to ${CERTS_DUMPER_RUNTIME_DIR}/ready. Compose healthchecks that file.

A failing Mailcow hook fails that cycle. The next poll retries. The image does not watch the live ACME file. MAILCOW_ENABLED accepts only true or false. Any other value exits before the poll loop.

With --domain-subdir the dump writes one directory per domain under ACME_DEST: <domain>/certificate.pem and <domain>/privatekey.pem. The Mailcow hook reads ${ACME_DEST}/${MAILCOW_SMTP_HOST}/.

Environment variables

The entrypoint reads these. Unset names use the default. ACME_FILENAME has no default.

Variable Default Purpose
ACME_FILENAME (required) Basename of the ACME JSON file inside ACME_DIR, for example cloudflare-acme.json.
ACME_DIR /data Directory that contains the ACME store. Mount it read-only.
ACME_DEST /data/files Directory for dumped PEM files. Mount it read-write.
CERTS_DUMPER_RUNTIME_DIR /run/certs-dumper Tmpfs for the ACME snapshot, the dump lock, and the ready marker.
CERTS_DUMPER_POLL_SECONDS 5 Seconds between poll cycles.
MAILCOW_ENABLED false true copies the dumped SMTP certificate to Mailcow. false dumps PEM only.

TZ is a normal container timezone. The image does not read it.

MAILCOW_ENABLED=false does not read SSH or the DNS token, so the container becomes ready after the first successful dump. true requires the variables and files below. The hook reads them only inside that run.

Variable Default Purpose
MAILCOW_SSH_HOST (required) SSH host that receives the certificate.
MAILCOW_SSH_USER certdeploy SSH user on that host.
MAILCOW_DNS_ZONE (required) Zone whose DNSSEC AD bit is checked before the TLSA write.
MAILCOW_SMTP_HOST (required) Hostname under ACME_DEST whose certificate.pem and privatekey.pem are copied.
MAILCOW_PROJECT_PATH /opt/mailcow-dockerized Mailcow compose directory on the SSH host.

The SSH private key is /run/secrets/TRAEFIK_CERTS_DUMPER_PASSWORD. The DNS token is /run/secrets/DNS_API_TOKEN (DNS_API_TOKEN_FILE overrides that path). Pinned host keys are /state/known_hosts (CERTS_DUMPER_SSH_KNOWN_HOSTS_FILE overrides that path). The hook does not write known_hosts and does not use accept-new. ACME_FILENAME must be cloudflare-acme.json or desec-acme.json. The hook updates one existing _25._tcp. TLSA record and restarts postfix-mailcow, dovecot-mailcow, and nginx-mailcow.

Compose

Use the template in docker-templates when this container sits next to Traefik. The shape below is that service: read-only ACME parent, writable PEM leaf, and a runtime tmpfs. CERTRESOLVER comes from the Traefik app .env and becomes ACME_FILENAME. Set MAILCOW_ENABLED to true only together with the SSH key, the DNS token, and the pinned known_hosts file.

services:
  traefik_certs-dumper:
    image: git.openso.de/openso/traefik-certs-dumper
    user: "1000:1000"
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    init: true
    tmpfs:
      - /run:rw,noexec,nosuid,nodev,size=64m
      - /run/certs-dumper:rw,noexec,nosuid,nodev,size=32m,mode=0700,uid=1000,gid=1000
      - /tmp:rw,noexec,nosuid,nodev,size=128m
    volumes:
      - ./appdata/config/certs:/data:ro
      - ./appdata/config/certs/files:/data/files:rw
    environment:
      TZ: Europe/Berlin
      ACME_FILENAME: ${CERTRESOLVER:-cloudflare}-acme.json
      MAILCOW_ENABLED: "false"
    healthcheck:
      test: ["CMD-SHELL", "test -f /run/certs-dumper/ready"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s

Create ./appdata/config/certs/files on the host before start. Traefik must already write ${CERTRESOLVER}-acme.json into ./appdata/config/certs. The container stays in the healthcheck start period until that file contains a certificate and the first dump succeeds. With MAILCOW_ENABLED=true the Mailcow hook must succeed in that same cycle.

Remote copy (Mailcow SSH, known_hosts, DNS TLSA) is documented on the template. Enable that package there; do not add secrets to this image repository.

Build

A push to main builds and pushes :main and :latest. A tag v* builds and pushes :<tag> and :latest. The first commit on main is that push, so the first image is produced without a manual build.

The image compiles github.com/ldez/traefik-certs-dumper v2.11.4 with Go 1.26.10. golang.org/x/net v0.60.0, golang.org/x/text v0.42.0, and google.golang.org/grpc v1.83.2 replace the versions in that tag so the image scan can clear fixed HIGH findings. Alpine packages are pinned to the versions resolved for Alpine 3.24. The runtime image also includes openssh-client, jq, curl, openssl, and bind-tools for the baked Mailcow hook.