From 2cfdc5851825157f1e42b558293c9673994b4f48 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sat, 25 Apr 2026 23:57:31 +0000 Subject: [PATCH] upgrade to v0.16 with declarative bootstrap --- stalwart/.env.example | 4 +++ stalwart/Dockerfile.bootstrap | 9 ++++++ stalwart/{ => data}/.gitignore | 2 +- stalwart/data/etc/config.json | 4 +++ stalwart/data/{ => var}/.gitkeep | 0 stalwart/docker-compose.yaml | 54 ++++++++++++++++++++++++++++++-- stalwart/plan.json | 1 + 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 stalwart/.env.example create mode 100644 stalwart/Dockerfile.bootstrap rename stalwart/{ => data}/.gitignore (55%) create mode 100644 stalwart/data/etc/config.json rename stalwart/data/{ => var}/.gitkeep (100%) create mode 100644 stalwart/plan.json diff --git a/stalwart/.env.example b/stalwart/.env.example new file mode 100644 index 0000000..8825748 --- /dev/null +++ b/stalwart/.env.example @@ -0,0 +1,4 @@ +STALWART_CF_TOKEN= +STALWART_RECOVERY_ADMIN= +STALWART_BOOTSTRAP_USER= +STALWART_BOOTSTRAP_PASSWORD= diff --git a/stalwart/Dockerfile.bootstrap b/stalwart/Dockerfile.bootstrap new file mode 100644 index 0000000..d0aea9a --- /dev/null +++ b/stalwart/Dockerfile.bootstrap @@ -0,0 +1,9 @@ +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates curl xz \ + && curl -fsSL https://github.com/stalwartlabs/cli/releases/download/v1.0.2/stalwart-cli-x86_64-unknown-linux-musl.tar.xz \ + | tar -xJ -C /tmp \ + && mv /tmp/stalwart-cli-*/stalwart-cli /usr/local/bin/stalwart-cli \ + && chmod +x /usr/local/bin/stalwart-cli \ + && rm -rf /tmp/* \ + && apk del curl xz +ENTRYPOINT ["/usr/local/bin/stalwart-cli"] diff --git a/stalwart/.gitignore b/stalwart/data/.gitignore similarity index 55% rename from stalwart/.gitignore rename to stalwart/data/.gitignore index 394be19..f6997ad 100644 --- a/stalwart/.gitignore +++ b/stalwart/data/.gitignore @@ -1,2 +1,2 @@ -/data/* +/var/* !.gitkeep diff --git a/stalwart/data/etc/config.json b/stalwart/data/etc/config.json new file mode 100644 index 0000000..96e85f3 --- /dev/null +++ b/stalwart/data/etc/config.json @@ -0,0 +1,4 @@ +{ + "@type": "RocksDb", + "path": "/var/lib/stalwart" +} diff --git a/stalwart/data/.gitkeep b/stalwart/data/var/.gitkeep similarity index 100% rename from stalwart/data/.gitkeep rename to stalwart/data/var/.gitkeep diff --git a/stalwart/docker-compose.yaml b/stalwart/docker-compose.yaml index aa56cd3..29ff2ee 100644 --- a/stalwart/docker-compose.yaml +++ b/stalwart/docker-compose.yaml @@ -1,4 +1,6 @@ networks: + stalwart: + external: false caddy: name: caddy driver: bridge @@ -9,10 +11,16 @@ services: image: stalwartlabs/stalwart:v0.16 container_name: stalwart restart: unless-stopped + user: "1001:1001" labels: - "com.centurylinklabs.watchtower.enable=true" networks: + - stalwart - caddy + environment: + STALWART_RECOVERY_ADMIN: "${STALWART_RECOVERY_ADMIN}" + STALWART_HTTPS_PORT: "443" + STALWART_CF_TOKEN: "${STALWART_CF_TOKEN}" ports: - "25:25" - "587:587" @@ -23,11 +31,51 @@ services: - "110:110" - "995:995" volumes: - - ./data:/opt/stalwart - - ../caddy/data/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.aykhans.me/mail.aykhans.me.crt:/opt/stalwart/cert/mail.aykhans.me.pem - - ../caddy/data/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.aykhans.me/mail.aykhans.me.key:/opt/stalwart/cert/mail.aykhans.me.priv.pem + - ./data/etc:/etc/stalwart + - ./data/var:/var/lib/stalwart logging: driver: "json-file" options: max-size: "100m" max-file: "3" + + stalwart-bootstrap: + build: + context: . + dockerfile: Dockerfile.bootstrap + container_name: stalwart-bootstrap + networks: + - stalwart + environment: + STALWART_URL: "http://stalwart:8080" + STALWART_USER: "${STALWART_BOOTSTRAP_USER}" + STALWART_PASSWORD: "${STALWART_BOOTSTRAP_PASSWORD}" + STALWART_DEFAULT_HOSTNAME: "mail.aykhans.me" + STALWART_DEFAULT_DOMAIN: "aykhans.me" + volumes: + - ./plan.json:/plan.json:ro + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + # 1) Apply plan.json (Domain, etc.); tolerate primaryKeyViolation as expected. + out=$$(stalwart-cli apply --file /plan.json --continue-on-error 2>&1) || true + echo "$$out" + real=$$(echo "$$out" | grep -E "^✗" | grep -v "primaryKeyViolation" || true) + [ -z "$$real" ] || { echo "Unexpected apply errors"; exit 1; } + + # 2) Resolve Domain id by name + DOMAIN_ID=$$(stalwart-cli query Domain 2>/dev/null | awk -v n="$$STALWART_DEFAULT_DOMAIN" 'NR>1 && $$2==n {print $$1; exit}') + [ -n "$$DOMAIN_ID" ] || { echo "Domain $$STALWART_DEFAULT_DOMAIN not found"; exit 1; } + + # 3) Idempotent SystemSettings update (singleton) + stalwart-cli update SystemSettings --field "defaultHostname=$$STALWART_DEFAULT_HOSTNAME" --field "defaultDomainId=$$DOMAIN_ID" + + # 4) Trigger settings reload so url_https recomputes (no restart needed) + stalwart-cli create Action/ReloadSettings --json "{}" + + echo "Bootstrap complete" + depends_on: + stalwart: + condition: service_healthy + restart: "no" diff --git a/stalwart/plan.json b/stalwart/plan.json new file mode 100644 index 0000000..36f9529 --- /dev/null +++ b/stalwart/plan.json @@ -0,0 +1 @@ +{"@type":"create","object":"Domain","value":{"aykhans-me":{"name":"aykhans.me","isEnabled":true,"dkimManagement":{"@type":"Automatic"}}}}