Deploy a fresh Stalwart and ihasmail, linked, in one command
deploy stands up Stalwart 0.16, ihasmail and (for a mail host) Caddy as a compose project: completes Stalwart's bootstrap over x:Bootstrap, links ihasmail over the private network, requests certificates for both Caddy (TLS-ALPN-01) and Stalwart (HTTP-01 through Caddy), makes the auto-ban safe behind the proxy, and proves the link by signing in through the webmail. --local gives a loopback-only pair. certs retries Stalwart's certificate; destroy removes a deployment. e2e/public.sh runs the whole mail-host path against Pebble with no internet involved.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Written by ihasmail-oneshot {{.Version}} for {{.Plan.Domain}}.
|
||||
#
|
||||
# Caddy holds ports 80 and 443 for two things that both want certificates for
|
||||
# some of the same names: Caddy itself, to serve HTTPS, and Stalwart, whose
|
||||
# IMAP and SMTP listeners need a certificate of their own. They are kept apart
|
||||
# by challenge type rather than by name:
|
||||
#
|
||||
# Caddy TLS-ALPN-01 on 443 -- for Stalwart's names it never uses port 80.
|
||||
# Stalwart HTTP-01 on 80, which Caddy forwards to it untouched.
|
||||
#
|
||||
# So neither answers the other's challenge, and neither needs the other's key.
|
||||
|
||||
{
|
||||
email {{.Plan.Email}}
|
||||
{{- if .Plan.ACMEDirectory}}
|
||||
acme_ca {{.Plan.ACMEDirectory}}
|
||||
{{- end}}
|
||||
{{- if .Plan.ACMECARoot}}
|
||||
acme_ca_root /etc/caddy/acme-ca-root.pem
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
# The webmail. Push arrives as Server-Sent Events, so responses are flushed as
|
||||
# they are written rather than buffered.
|
||||
{{.Plan.WebmailHost}} {
|
||||
encode zstd gzip
|
||||
reverse_proxy ihasmail:8080 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
|
||||
# Stalwart's web side: its admin UI, JMAP for other clients, CalDAV, CardDAV,
|
||||
# autoconfig and MTA-STS. Stalwart is told to believe the X-Forwarded-For Caddy
|
||||
# sets here, so a scanner is banned by its own address and not by Caddy's.
|
||||
{{join .Plan.StalwartNames ", "}} {
|
||||
tls {
|
||||
issuer acme {
|
||||
{{- if .Plan.ACMEDirectory}}
|
||||
dir {{.Plan.ACMEDirectory}}
|
||||
{{- end}}
|
||||
{{- if .Plan.ACMECARoot}}
|
||||
trusted_roots /etc/caddy/acme-ca-root.pem
|
||||
{{- end}}
|
||||
email {{.Plan.Email}}
|
||||
disable_http_challenge
|
||||
}
|
||||
}
|
||||
reverse_proxy stalwart:8080
|
||||
}
|
||||
|
||||
# Port 80 for Stalwart's names is Stalwart's challenge path and a redirect.
|
||||
{{range $i, $n := .Plan.StalwartNames}}{{if $i}}, {{end}}http://{{$n}}{{end}} {
|
||||
handle /.well-known/acme-challenge/* {
|
||||
reverse_proxy stalwart:8080
|
||||
}
|
||||
handle {
|
||||
redir https://{host}{uri} 308
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# Written by ihasmail-oneshot {{.Version}} for {{.Plan.Domain}}.
|
||||
#
|
||||
# This is the whole deployment: bring it up again with `docker compose up -d`
|
||||
# from this directory. Secrets are in .env next to it, and the Stalwart
|
||||
# administrator's password is in credentials.txt -- both readable only by you.
|
||||
#
|
||||
# Stalwart's plain-HTTP port is reachable only on the private network below and
|
||||
# on {{.Plan.StalwartBind}}. ihasmail talks to it over that network, which is
|
||||
# why STALWART_URL is http://: the leg never leaves this host.
|
||||
name: {{.Plan.Project}}
|
||||
|
||||
services:
|
||||
stalwart:
|
||||
image: {{.Plan.StalwartImage}}
|
||||
hostname: {{.Plan.MailHost}}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "{{.Plan.StalwartBind}}:8080"
|
||||
{{- range .Plan.PublishedPorts}}{{if and (ne . 80) (ne . 443)}}
|
||||
- "{{.}}:{{.}}"
|
||||
{{- end}}{{end}}
|
||||
volumes:
|
||||
- stalwart-etc:/etc/stalwart
|
||||
- stalwart-data:/var/lib/stalwart
|
||||
{{- if .CABundle}}
|
||||
# The system roots plus the private ACME CA, so Stalwart can reach it.
|
||||
- ./ca-bundle.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||
{{- end}}
|
||||
networks:
|
||||
stack:
|
||||
ipv4_address: {{.Plan.StalwartIP}}
|
||||
|
||||
ihasmail:
|
||||
image: {{.Plan.IhasmailImage}}
|
||||
restart: unless-stopped
|
||||
depends_on: [stalwart]
|
||||
# Immutable: read-only root, no volume, sessions in memory. A restart signs
|
||||
# everyone out; nothing else is lost, because nothing else is kept here.
|
||||
read_only: true
|
||||
tmpfs: [/tmp]
|
||||
ports:
|
||||
- "{{.Plan.WebmailBind}}:8080"
|
||||
environment:
|
||||
STALWART_URL: http://stalwart:8080
|
||||
APP_SECRET: ${APP_SECRET:?APP_SECRET is missing from .env}
|
||||
IMMUTABLE: "1"
|
||||
SESSION_FILE: ""
|
||||
TRUST_PROXY: "1"
|
||||
IMAGE_PROXY: "1"
|
||||
{{- if not .Plan.Local}}
|
||||
# Stalwart pushes changes to this URL instead of holding a connection per
|
||||
# tab. If it cannot reach it, every tab uses the relay; nothing breaks.
|
||||
PUSH_URL: https://{{.Plan.WebmailHost}}
|
||||
{{- end}}
|
||||
networks:
|
||||
stack:
|
||||
ipv4_address: {{.Plan.IhasmailIP}}
|
||||
{{- if not .Plan.Local}}
|
||||
|
||||
caddy:
|
||||
image: {{.Plan.CaddyImage}}
|
||||
restart: unless-stopped
|
||||
depends_on: [ihasmail, stalwart]
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
{{- if .Plan.ACMECARoot}}
|
||||
- ./acme-ca-root.pem:/etc/caddy/acme-ca-root.pem:ro
|
||||
{{- end}}
|
||||
networks:
|
||||
stack:
|
||||
ipv4_address: {{.Plan.CaddyIP}}
|
||||
{{- end}}
|
||||
|
||||
networks:
|
||||
stack:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: {{.Plan.Subnet}}
|
||||
|
||||
volumes:
|
||||
stalwart-etc:
|
||||
stalwart-data:
|
||||
{{- if not .Plan.Local}}
|
||||
# Certificates and the ACME account. Losing this means asking for every
|
||||
# certificate again, which is how rate limits are reached.
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
{{- end}}
|
||||
Reference in New Issue
Block a user