The plan now happens. "inbuxa install --local --domain example.test --install-deps --yes" on a machine with nothing on it ends with a mail server, a console and a webmail running, an administrator and a first mailbox created, and the records the domain needs written out. The sequence is the one ihasmail-oneshot worked out against a running server, which is why its JMAP client and its Docker handling came across nearly whole: bring the server up in bootstrap mode with a credential that lives in an override file for that step only, complete bootstrap, bring the rest up without it -- so no recovery credential outlives the setup -- exempt the front ends from the auto-ban, restart for the settings that need it, create the first account, and write down the password nothing else holds. New here: three services rather than two. The console is static files that learn their server's address at start, and the webmail is given the first-party OAuth client secret that the server is given too. Twenty checks in the lab, from a bare Debian 13. The two worth having are the ones that catch an install that looks fine and is not: nothing in the running server carries a recovery admin any more, and the account the installer created can sign in to the webmail it installed. Two bugs the lab caught, both of which would have shipped: - the private addresses were worked out on a copy of the stack, so the server was told the webmail speaks from "", and refused it. - the console image rewrites index.html when it starts, so a read-only root filesystem left it restarting forever. The webmail keeps read_only; the console cannot have it until that rewrite moves.
137 lines
4.1 KiB
Cheetah
137 lines
4.1 KiB
Cheetah
# Written by the inbuxa installer {{.Version}} for {{.Domain}}.
|
|
#
|
|
# This is the whole deployment. Bring it up again with `docker compose up -d`
|
|
# from this directory. Secrets are in .env beside it and the administrator's
|
|
# password is in credentials.txt -- both readable only by root.
|
|
#
|
|
# The mail server's plain HTTP port is published on loopback only and reached
|
|
# over the private network below, which is why the front ends talk to it as
|
|
# http://: that leg never leaves this machine.
|
|
name: {{.Project}}
|
|
|
|
services:
|
|
server:
|
|
image: {{.ServerImage}}
|
|
hostname: {{.MailHost}}
|
|
restart: unless-stopped
|
|
ports:
|
|
- "{{.ServerBind}}:8080"
|
|
{{- range .MailPorts}}
|
|
- "{{.}}:{{.}}"
|
|
{{- end}}
|
|
volumes:
|
|
- inbuxa-etc:/opt/stalwart/etc
|
|
- inbuxa-data:/opt/stalwart/data
|
|
{{- if .CABundle}}
|
|
# The system roots plus the private ACME CA, so the server can reach it.
|
|
- ./ca-bundle.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
|
{{- end}}
|
|
environment:
|
|
# The front ends, which is what registers their OAuth clients and sets
|
|
# the cross-origin allowlist (contract C-6 and C-14). The webmail's
|
|
# secret is generated by the installer and given to both sides.
|
|
{{- if .ConsoleURL}}
|
|
INBUXA_ADMIN_URL: {{.ConsoleURL}}
|
|
{{- end}}
|
|
{{- if .WebmailURL}}
|
|
INBUXA_WEBMAIL_URL: {{.WebmailURL}}
|
|
INBUXA_WEBMAIL_CLIENT_SECRET: ${WEBMAIL_CLIENT_SECRET:?WEBMAIL_CLIENT_SECRET is missing from .env}
|
|
{{- end}}
|
|
networks:
|
|
stack:
|
|
ipv4_address: {{.ServerIP}}
|
|
{{- if .Console}}
|
|
|
|
console:
|
|
image: {{.ConsoleImage}}
|
|
restart: unless-stopped
|
|
depends_on: [server]
|
|
# Not read-only, unlike the webmail: this image writes the api-base-url
|
|
# into index.html when it starts, which is what lets one image serve any
|
|
# installation. A read-only root makes it restart forever instead.
|
|
tmpfs: [/tmp, /var/cache/nginx, /var/run]
|
|
ports:
|
|
- "{{.ConsoleBind}}:8080"
|
|
environment:
|
|
# Written into index.html at start, so one image serves any
|
|
# installation. The browser talks to the server directly from here.
|
|
API_BASE_URL: {{.ServerPublicURL}}
|
|
networks:
|
|
stack:
|
|
ipv4_address: {{.ConsoleIP}}
|
|
{{- end}}
|
|
{{- if .Webmail}}
|
|
|
|
webmail:
|
|
image: {{.WebmailImage}}
|
|
restart: unless-stopped
|
|
depends_on: [server]
|
|
# Immutable: read-only root, no volume, sessions in memory. A restart
|
|
# signs everyone out; nothing else is lost, because nothing else is kept.
|
|
read_only: true
|
|
tmpfs: [/tmp]
|
|
ports:
|
|
- "{{.WebmailBind}}:8080"
|
|
environment:
|
|
MAIL_SERVER_URL: http://server:8080
|
|
APP_SECRET: ${APP_SECRET:?APP_SECRET is missing from .env}
|
|
APP_NAME: inbuxa
|
|
IMMUTABLE: "1"
|
|
SESSION_FILE: ""
|
|
TRUST_PROXY: "1"
|
|
IMAGE_PROXY: "1"
|
|
{{- if not .Local}}
|
|
# Sign-in goes through the mail server's own page, as the first-party
|
|
# client the server registered for this URL.
|
|
OAUTH_CLIENT_ID: ihasmail-inbuxa
|
|
OAUTH_CLIENT_SECRET: ${WEBMAIL_CLIENT_SECRET:?WEBMAIL_CLIENT_SECRET is missing from .env}
|
|
PUBLIC_URL: {{.WebmailURL}}
|
|
{{- if .ConsoleURL}}
|
|
ADMIN_URL: {{.ConsoleURL}}
|
|
{{- end}}
|
|
# The server pushes changes here instead of holding a connection per
|
|
# tab. If it cannot reach it, every tab falls back to the relay.
|
|
PUSH_URL: {{.WebmailURL}}
|
|
{{- end}}
|
|
networks:
|
|
stack:
|
|
ipv4_address: {{.WebmailIP}}
|
|
{{- end}}
|
|
{{- if .Proxy}}
|
|
|
|
caddy:
|
|
image: {{.CaddyImage}}
|
|
restart: unless-stopped
|
|
depends_on: [server]
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "443:443/udp"
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy-data:/data
|
|
- caddy-config:/config
|
|
{{- if .ACMECARoot}}
|
|
- ./acme-ca-root.pem:/etc/caddy/acme-ca-root.pem:ro
|
|
{{- end}}
|
|
networks:
|
|
stack:
|
|
ipv4_address: {{.CaddyIP}}
|
|
{{- end}}
|
|
|
|
networks:
|
|
stack:
|
|
ipam:
|
|
config:
|
|
- subnet: {{.Subnet}}
|
|
|
|
volumes:
|
|
inbuxa-etc:
|
|
inbuxa-data:
|
|
{{- if .Proxy}}
|
|
# 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}}
|