From 7de657c392074e8a27a88fd627e529c30a4e2495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 16:01:44 +0100 Subject: [PATCH 1/7] feat: pass Chatwoot env vars to web (no-op when unset) CHATWOOT_MAC_TOKEN and CHATWOOT_BASE_URL are pure passthroughs used by the hosted app for Chatwoot support auth; when unset in .env, Compose substitutes empty strings and the web app treats them as disabled. Same pattern as Stripe, Loops, S3, etc. --- docker-compose.yaml | 2 ++ example.env | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index ce4bb65..18e99fe 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -74,6 +74,8 @@ services: - S3_HOST=${S3_HOST} - LOOPS_API_KEY=${LOOPS_API_KEY} - LOOPS_ACTIVE_USERS_LIST_ID=${LOOPS_ACTIVE_USERS_LIST_ID} + - CHATWOOT_MAC_TOKEN=${CHATWOOT_MAC_TOKEN} + - CHATWOOT_BASE_URL=${CHATWOOT_BASE_URL} caddy: image: ghcr.io/shroud-email/caddy-permissive-file-storage:main diff --git a/example.env b/example.env index f4c06fc..d544431 100644 --- a/example.env +++ b/example.env @@ -30,6 +30,11 @@ AWS_SECRET_ACCESS_KEY=replace S3_BUCKET=replace S3_HOST=replace +# CHATWOOT_MAC_TOKEN / CHATWOOT_BASE_URL: optional, for Chatwoot support +# auth on the hosted app. Leave blank if unused. +CHATWOOT_MAC_TOKEN= +CHATWOOT_BASE_URL= + ## You can leave the defaults for the remaining options. DB_USER=postgres From c881717925c4ff2996a13017bb122c156889072e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 16:06:18 +0100 Subject: [PATCH 2/7] chore: untrack per-deployment Haraka me hostname, ship template haraka/haraka_config/config/me holds the SMTP greeting hostname, which is per-deployment, yet it was both tracked in git and listed in .gitignore. That made git status permanently dirty on every deployment and a careless 'git checkout .' would clobber the real hostname back to 'Shroud.email'. Untrack it (the .gitignore entry already keeps the local copy on disk), and ship a tracked me.example template instead. Self-hosters copy it to me and set their hostname. .gitignore is unchanged. --- README.md | 2 ++ haraka/haraka_config/config/me | 1 - haraka/haraka_config/config/me.example | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 haraka/haraka_config/config/me create mode 100644 haraka/haraka_config/config/me.example diff --git a/README.md b/README.md index a77688a..f3b33f6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Please read our [deployment documentation](https://shroud.email/docs/deployment/ If you just want to get up and running with Shroud.email quickly, you can sign up for our hosted version [here](https://app.shroud.email/users/register). +Copy `haraka/haraka_config/config/me.example` to `haraka/haraka_config/config/me` and set your mail hostname. + ## Living on the edge The committed `docker-compose.yaml` tracks the stable `:1` image. If you'd rather diff --git a/haraka/haraka_config/config/me b/haraka/haraka_config/config/me deleted file mode 100644 index fb63837..0000000 --- a/haraka/haraka_config/config/me +++ /dev/null @@ -1 +0,0 @@ -Shroud.email \ No newline at end of file diff --git a/haraka/haraka_config/config/me.example b/haraka/haraka_config/config/me.example new file mode 100644 index 0000000..de54ac6 --- /dev/null +++ b/haraka/haraka_config/config/me.example @@ -0,0 +1 @@ +example.com From b71d5a95b66f037b9a9c2b09bb710edd80f6a758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 16:29:33 +0100 Subject: [PATCH 3/7] feat: opt-in Bunny DNS-01 TLS for Caddy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caddy is now built locally (caddy/Dockerfile) with xcaddy v2.10.0, bundling caddy-permissive-file-storage (pinned to commit 4059a60, the PR #1 fix for the modern context-based Store() signature — not yet released as a tag) and caddy-dns/bunny v1.2.0. Self-hosters keep the default HTTP-01 ACME (caddy/Caddyfile unchanged). DNS-01 via Bunny is opt-in: set BUNNY_API_KEY and CADDYFILE_PATH=./caddy/Caddyfile.bunny in .env. The compose caddy service now builds from ./caddy, passes BUNNY_API_KEY through, and mounts ${CADDYFILE_PATH:-./caddy/Caddyfile} so the active Caddyfile is selectable without editing compose. Verified: image builds, caddy version v2.10.0, both caddy.storage.permissive_file_storage and dns.providers.bunny modules present, and both Caddyfile and Caddyfile.bunny validate. --- README.md | 14 ++++++++++++++ caddy/Caddyfile.bunny | 21 +++++++++++++++++++++ caddy/Dockerfile | 20 ++++++++++++++++++++ docker-compose.yaml | 5 +++-- example.env | 11 +++++++++++ 5 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 caddy/Caddyfile.bunny create mode 100644 caddy/Dockerfile diff --git a/README.md b/README.md index f3b33f6..524184c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ If you just want to get up and running with Shroud.email quickly, you can sign u Copy `haraka/haraka_config/config/me.example` to `haraka/haraka_config/config/me` and set your mail hostname. +## TLS via Bunny DNS-01 (optional) + +Caddy defaults to HTTP-01 ACME (port 80), which works behind no other reverse +proxy. If your setup needs DNS-01 (e.g. you can't open port 80, or you want +wildcard certs), opt in to the Bunny.net DNS challenge: + +1. Set `BUNNY_API_KEY` in `.env` to your Bunny.net account API key. +2. Set `CADDYFILE_PATH=./caddy/Caddyfile.bunny` in `.env`. +3. `docker compose up -d --build caddy`. + +The Caddy binary is built locally (see `caddy/Dockerfile`) with both the +`caddy-permissive-file-storage` and `caddy-dns/bunny` modules. Self-hosters who +leave the defaults get HTTP-01 and never need a Bunny key. + ## Living on the edge The committed `docker-compose.yaml` tracks the stable `:1` image. If you'd rather diff --git a/caddy/Caddyfile.bunny b/caddy/Caddyfile.bunny new file mode 100644 index 0000000..020ec1d --- /dev/null +++ b/caddy/Caddyfile.bunny @@ -0,0 +1,21 @@ +{ + # Global options + storage permissive_file_storage { + root "/data" + } + + email {$ADMIN_EMAIL} +} + +{$APP_DOMAIN} { + reverse_proxy web:8080 + tls { + dns bunny {env.BUNNY_API_KEY} + } +} + +{$EMAIL_DOMAIN} { + tls { + dns bunny {env.BUNNY_API_KEY} + } +} diff --git a/caddy/Dockerfile b/caddy/Dockerfile new file mode 100644 index 0000000..f36745f --- /dev/null +++ b/caddy/Dockerfile @@ -0,0 +1,20 @@ +# Compile Caddy with custom modules. +# - caddy-permissive-file-storage: world-readable certs so the cron +# sidecar can bundle them for Haraka (the whole reason this fork exists). +# - caddy-dns/bunny: DNS-01 ACME via Bunny.net, opt-in via Caddyfile.bunny. +# +# Built locally by docker compose (see `caddy` service `build: ./caddy`). +# Pins: Caddy v2.10.0, bunny v1.2.0. caddy-permissive-file-storage is pinned to +# commit 4059a60 (PR #1, "Update for current Caddy") because the latest tag +# v0.1.4 still has the pre-context Store() signature that fails to compile +# against Caddy v2.10.0+. Replace with @v0.1.5 once a release is cut. +FROM caddy:2.10-builder AS builder + +RUN xcaddy build v2.10.0 \ + --with github.com/Shroud-email/caddy-permissive-file-storage@4059a602db55a1762de165dac7f1204c823ebbd4 \ + --with github.com/caddy-dns/bunny@v1.2.0 + +# Production image +FROM caddy:2.10-alpine + +COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/docker-compose.yaml b/docker-compose.yaml index 18e99fe..3810cb1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,18 +78,19 @@ services: - CHATWOOT_BASE_URL=${CHATWOOT_BASE_URL} caddy: - image: ghcr.io/shroud-email/caddy-permissive-file-storage:main + build: ./caddy restart: unless-stopped environment: - ADMIN_EMAIL=${ADMIN_EMAIL} - APP_DOMAIN=${APP_DOMAIN} - EMAIL_DOMAIN=${EMAIL_DOMAIN} + - BUNNY_API_KEY=${BUNNY_API_KEY} ports: - "80:80" - "443:443" volumes: - caddy_data:/data - - ./caddy/Caddyfile:/etc/caddy/Caddyfile + - ${CADDYFILE_PATH:-./caddy/Caddyfile}:/etc/caddy/Caddyfile cron: build: ./cron diff --git a/example.env b/example.env index d544431..5ed78d9 100644 --- a/example.env +++ b/example.env @@ -35,6 +35,17 @@ S3_HOST=replace CHATWOOT_MAC_TOKEN= CHATWOOT_BASE_URL= +# BUNNY_API_KEY: optional. Bunny.net API key for DNS-01 ACME TLS issuance in +# Caddy. Only used if you opt in by setting CADDYFILE_PATH to +# ./caddy/Caddyfile.bunny below. Leave blank to keep the default HTTP-01. +BUNNY_API_KEY= + +# CADDYFILE_PATH: optional. Path to the Caddyfile to mount into the caddy +# container. Defaults to ./caddy/Caddyfile (HTTP-01). Set to +# ./caddy/Caddyfile.bunny to use DNS-01 via Bunny (requires BUNNY_API_KEY). +# Self-hosters can leave the default. +CADDYFILE_PATH=./caddy/Caddyfile + ## You can leave the defaults for the remaining options. DB_USER=postgres From 0089aba956b8316a34c663b9567252013bd3b206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 16:44:06 +0100 Subject: [PATCH 4/7] caddy: track caddy-permissive-file-storage @main Switch the Dockerfile pin from commit 4059a60 to @main, matching the repo's prior rolling-:main approach. The PR #1 "Update for current Caddy" fix lives on main; tracking main means we pick up future fixes without manual pin bumps. Rebuilt and verified: caddy v2.10.0, both permissive_file_storage and bunny modules present, both Caddyfiles validate. --- caddy/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/caddy/Dockerfile b/caddy/Dockerfile index f36745f..80e8658 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -4,14 +4,14 @@ # - caddy-dns/bunny: DNS-01 ACME via Bunny.net, opt-in via Caddyfile.bunny. # # Built locally by docker compose (see `caddy` service `build: ./caddy`). -# Pins: Caddy v2.10.0, bunny v1.2.0. caddy-permissive-file-storage is pinned to -# commit 4059a60 (PR #1, "Update for current Caddy") because the latest tag +# Pins: Caddy v2.10.0, bunny v1.2.0. caddy-permissive-file-storage tracks +# main (the PR #1 "Update for current Caddy" fix lives there; the latest tag # v0.1.4 still has the pre-context Store() signature that fails to compile -# against Caddy v2.10.0+. Replace with @v0.1.5 once a release is cut. +# against Caddy v2.10.0+). FROM caddy:2.10-builder AS builder RUN xcaddy build v2.10.0 \ - --with github.com/Shroud-email/caddy-permissive-file-storage@4059a602db55a1762de165dac7f1204c823ebbd4 \ + --with github.com/Shroud-email/caddy-permissive-file-storage@main \ --with github.com/caddy-dns/bunny@v1.2.0 # Production image From ada1bec9036f4e28e08c96d77eb10d926c9c6c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 16:58:05 +0100 Subject: [PATCH 5/7] caddy: keep EMAIL_DOMAIN on HTTP-01 acme in Caddyfile.bunny MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prod's known-working config only uses Bunny DNS-01 for APP_DOMAIN; the email domain stays on issuer acme (HTTP-01). The Bunny variant was stricter than prod and would require Bunny to be authoritative DNS for EMAIL_DOMAIN too. Revert that one block to match prod — only APP_DOMAIN uses dns bunny. --- caddy/Caddyfile.bunny | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/Caddyfile.bunny b/caddy/Caddyfile.bunny index 020ec1d..ab5b53d 100644 --- a/caddy/Caddyfile.bunny +++ b/caddy/Caddyfile.bunny @@ -16,6 +16,6 @@ {$EMAIL_DOMAIN} { tls { - dns bunny {env.BUNNY_API_KEY} + issuer acme } } From 2d95debcdde245cca72f592e4d59371ad99ca5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 17:00:37 +0100 Subject: [PATCH 6/7] fix: pin Watchtower DOCKER_API_VERSION for Docker Engine 28+ Watchtower 1.7.1's Docker SDK defaults to Docker API version 1.25, too old for Docker Engine 28+. On modern hosts the override silently fails to talk to dockerd. Pin the API version explicitly via DOCKER_API_VERSION=1.44 in the watchtower service environment list. --- docker-compose.override.example.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.override.example.yaml b/docker-compose.override.example.yaml index ceca03b..bc83543 100644 --- a/docker-compose.override.example.yaml +++ b/docker-compose.override.example.yaml @@ -22,3 +22,4 @@ services: - WATCHTOWER_LABEL_ENABLE=true # ONLY update opted-in containers — never db - WATCHTOWER_POLL_INTERVAL=300 # poll GHCR every 5 min - WATCHTOWER_CLEANUP=true # delete old images after updating + - DOCKER_API_VERSION=1.44 # watchtower 1.7.1's SDK defaults to 1.25, too old for Docker Engine 28+ From 268750c213d97c268fa4745ea2301ed95cb650c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bojl=C3=A9n?= Date: Fri, 24 Jul 2026 17:05:41 +0100 Subject: [PATCH 7/7] caddy: drop verbose pin-rationale comment from Dockerfile --- caddy/Dockerfile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 80e8658..9ee4ac8 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -2,12 +2,6 @@ # - caddy-permissive-file-storage: world-readable certs so the cron # sidecar can bundle them for Haraka (the whole reason this fork exists). # - caddy-dns/bunny: DNS-01 ACME via Bunny.net, opt-in via Caddyfile.bunny. -# -# Built locally by docker compose (see `caddy` service `build: ./caddy`). -# Pins: Caddy v2.10.0, bunny v1.2.0. caddy-permissive-file-storage tracks -# main (the PR #1 "Update for current Caddy" fix lives there; the latest tag -# v0.1.4 still has the pre-context Store() signature that fails to compile -# against Caddy v2.10.0+). FROM caddy:2.10-builder AS builder RUN xcaddy build v2.10.0 \