Install the suite, for real, in containers
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.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Written by the inbuxa installer {{.Version}} for {{.Domain}}.
|
||||
#
|
||||
# Caddy holds 80 and 443 for two programs that both want certificates for some
|
||||
# of the same names: Caddy itself, to serve HTTPS, and the mail server, 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 the server's names it never uses 80.
|
||||
# the server 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 {{.Email}}
|
||||
{{- if .ACMEDirectory}}
|
||||
acme_ca {{.ACMEDirectory}}
|
||||
{{- end}}
|
||||
{{- if .ACMECARoot}}
|
||||
acme_ca_root /etc/caddy/acme-ca-root.pem
|
||||
{{- end}}
|
||||
}
|
||||
{{- if .Webmail}}
|
||||
|
||||
# The webmail. Push arrives as Server-Sent Events, so responses are flushed as
|
||||
# they are written rather than buffered.
|
||||
{{.WebmailHost}} {
|
||||
encode zstd gzip
|
||||
reverse_proxy webmail:8080 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
{{- end}}
|
||||
{{- if .Console}}
|
||||
|
||||
# The console. Static files: it talks to the mail server from the browser, so
|
||||
# nothing here proxies the API.
|
||||
{{.ConsoleHost}} {
|
||||
encode zstd gzip
|
||||
reverse_proxy console:8080
|
||||
}
|
||||
{{- end}}
|
||||
|
||||
# The mail server's web side: JMAP for the front ends and any other client,
|
||||
# CalDAV, CardDAV, autoconfig and MTA-STS. The server is told to believe the
|
||||
# X-Forwarded-For Caddy sets here, so a scanner is banned by its own address
|
||||
# rather than by Caddy's.
|
||||
{{join .ServerNames ", "}} {
|
||||
tls {
|
||||
issuer acme {
|
||||
{{- if .ACMEDirectory}}
|
||||
dir {{.ACMEDirectory}}
|
||||
{{- end}}
|
||||
{{- if .ACMECARoot}}
|
||||
trusted_roots /etc/caddy/acme-ca-root.pem
|
||||
{{- end}}
|
||||
email {{.Email}}
|
||||
disable_http_challenge
|
||||
}
|
||||
}
|
||||
reverse_proxy server:8080
|
||||
}
|
||||
|
||||
# Port 80 for the server's names is its challenge path and a redirect.
|
||||
{{range $i, $n := .ServerNames}}{{if $i}}, {{end}}http://{{$n}}{{end}} {
|
||||
handle /.well-known/acme-challenge/* {
|
||||
reverse_proxy server:8080
|
||||
}
|
||||
handle {
|
||||
redir https://{host}{uri} 308
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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}}
|
||||
Reference in New Issue
Block a user