9.3 KiB
Docker deployment
Three independent images, all built from the same source tree:
Dockerfile— mailgoserver only.Dockerfile.rspamd— mailgoserver + the latest rspamd in the same container, already wired together. Use this if you want stronger spam filtering than the built-in heuristic score alone, but don't need Bayes learning or greylisting.Dockerfile.aio— mailgoserver + rspamd + redis, the full spam-filtering stack: redis backs rspamd's Bayes classifier and greylisting module, neither of which work without it. mailgoserver itself has no direct use for redis — it's a single-instance app already backed by SQLite for everything, so this exists purely to make rspamd's scoring meaningfully better, not to make mailgoserver itself faster or more scalable.
docker-compose.yml defines all three as Compose profiles so a plain docker compose up can't accidentally start more than one at once:
cd docker-deploy
docker compose --profile standalone up -d --build # mailserver only
docker compose --profile with-rspamd up -d --build # mailserver + rspamd
docker compose --profile all-in-one up -d --build # mailserver + rspamd + redis
Either way, the app itself binds the real standard mail ports by default — 25
(SMTP), 465 (direct-TLS SMTP), 143 (IMAP), 993 (direct-TLS IMAP) — and the admin/webmail
UI on its usual non-privileged 5000/5001 (HTTP/HTTPS); put your own reverse proxy or a
80:5000/443:5001 port mapping in front if you want those on 80/443 too (note port 80
is already published here for Let's Encrypt HTTP-01, see below — pick a different host
port for the web UI's 80 mapping if you use both). Copy .env.example to .env in this
folder to change any host-side port — useful if something else on the host already owns
25/143/etc., or if you want to run more than one profile side by side.
Security: nothing here runs as root
None of the three images run mailgoserver, rspamd, or redis as root. Each gets its own
unprivileged system user (rspamd's and redis's own .deb packages create theirs;
mailgoserver's is created in the Dockerfile), and mailgoserver's binary is granted just
the one Linux capability it actually needs — CAP_NET_BIND_SERVICE, to bind ports
25/465/80 — via setcap at build time, rather than the whole container running as root
to get the same effect. CAP_NET_BIND_SERVICE is one of Docker's default capabilities,
so this needs no extra --cap-add at docker run/compose time.
The Dockerfile.rspamd/Dockerfile.aio entrypoint scripts (entrypoint-rspamd.sh,
entrypoint-aio.sh) do still start as root — only long enough to chown the
persistent subdirectories under server_data/ for whichever user needs to write there,
then drop to that user (via runuser) for every actual process, mailgoserver included.
What happens on first boot
There's no baked-in config. On first start, the binary generates a fresh
server_data/settings.ini with defaults (mirroring internal/config/config.go), a
self-signed TLS certificate, DKIM/mailstore master keys, and an empty SQLite database —
all inside the server_data/ volume, so it survives container restarts/rebuilds. The
web UI seeds one admin account: username admin, password Password123!, and forces an
immediate username + password change on first login — see the main
README for the full first-login walkthrough.
Before using this for real mail, exec into the container (or edit the volume from
the host) and update settings.ini:
docker exec -it mailgoserver sh -c 'vi /app/server_data/settings.ini'
docker restart mailgoserver
At minimum, set [Server] HOSTNAME / helo_hostname to your real mail domain, and if
you'll use passkeys, [Auth] rp_id / rp_origin to match the exact domain the admin
dashboard is reached at (rp_id can't be localhost once you're on a real domain —
see the main README's WebAuthn note). There's no environment-variable override
mechanism — settings.ini in the volume is the one source of config truth.
Enabling rspamd (with-rspamd and all-in-one)
The bundled rspamd's default config already listens on 127.0.0.1:11333 for scanning
requests — exactly what [Rspamd] url defaults to in settings.ini, and since both
processes share the container's network namespace, no networking setup is needed. All
that's left is turning it on:
[Rspamd]
enabled = true
url = http://127.0.0.1:11333
reject_score = 15
...then restart the container. The built-in heuristic spam score
(internal/mailstore/spam.go) always runs regardless of this setting — rspamd is
additive, not a replacement, and if it's ever unreachable, mail still flows on the
heuristic score alone (rspamd errors are logged, never fatal to delivery).
with-rspamd intentionally skips redis — rspamd runs fine without it for SPF/DKIM/RBL/
regexp-based scoring, but Bayes learning and greylisting need it. If you want those,
use all-in-one instead: its rspamd is already pre-configured (local.d/redis.conf,
classifier-bayes.conf, greylist.conf baked into the image) to use the redis running
alongside it — nothing to turn on beyond [Rspamd] enabled = true above. That redis
instance is loopback-only and has no exposed port or password: the only two processes
that can ever reach it are rspamd and mailgoserver, both inside the same container's
network namespace. If you publish 6379 yourself for some other reason, add a
requirepass to the redis-server command in entrypoint-aio.sh first.
Let's Encrypt HTTP-01 (no DNS provider needed)
If this domain's DNS isn't hosted anywhere the app can automate, enable HTTP-01 on the
admin dashboard's Let's Encrypt page and restart the container — it runs independently
alongside (or instead of) the DNS-01 flow above, obtaining its own separate certificate.
Port 80 (already published by docker-compose.yml) stays bound for the container's
whole lifetime once enabled, not just during an obtain — curl it and you should get a
plain 200 ok, the quickest way to confirm your port-forwarding/reverse-proxy setup
actually reaches this container. Optionally also request the certificate for this
container's public IP address (autodetected, or a manual override) so clients connecting
by bare IP get a trusted cert too — note this uses Let's Encrypt's shortlived profile,
so those certificates renew roughly every few days instead of every couple months
(handled automatically).
Which listener actually uses which certificate — the DNS-01 cert, the HTTP-01 cert, or the custom/self-signed one — is chosen independently per listener (SMTP-TLS, IMAP-TLS, web UI) on the admin dashboard's Settings page. A common setup: HTTP-01 for SMTP/IMAP, DNS-01 (or a real custom cert) for the web UI.
Persistence
Every profile mounts exactly one volume:
| Volume | What's in it |
|---|---|
mailserver-data (standalone) |
settings.ini, the SQLite DB, encrypted mailbox storage, DKIM/mailstore master keys, TLS certs, the CSRF app secret — everything mailgoserver itself owns. |
mailserver-rspamd-data (with-rspamd) |
The above, plus rspamd/ — rspamd's own fuzzy-hash storage and DNS/maps cache. |
mailserver-aio-data (all-in-one) |
The above, plus redis/ — redis's RDB snapshot, so Bayes-learning and greylist state survive restarts too. |
Back up the volume like you would the equivalent bare-metal server_data/ directory —
losing the mailstore master key makes all stored mail unrecoverable, same as a
non-Docker install. (Redis's own state isn't precious the same way — losing it just
means rspamd re-learns Bayes classifications and greylist history from scratch, not
anything mail-data-critical.)
Keeping the base image and packages patched
None of these images auto-update their own OS packages (Debian, rspamd, redis) while running — a container's filesystem is meant to be rebuilt from a fresh base, not patched in place, so the reliable way to pick up security fixes is rebuilding periodically:
docker compose --profile <profile> build --pull --no-cache
docker compose --profile <profile> up -d
Put that on a host-level cron/systemd timer (weekly is reasonable), or use a tool like
Renovate or Watchtower against this repo/image if you
want it automated end-to-end. Running apt-get upgrade on a schedule inside the
running container was considered and deliberately left out — it would only patch that
one running instance until its next recreation (at which point the image's original,
un-patched packages come back anyway), doesn't rebuild the Go binary itself, and adds a
cron daemon + package-manager attack surface to a container that's otherwise trying to
run three unprivileged, single-purpose processes. Rebuilding the image is both more
thorough and simpler.
Logs / health
docker compose --profile standalone logs -f # or --profile with-rspamd / all-in-one
docker inspect --format '{{.State.Health.Status}}' mailgoserver
All three images log to stdout/stderr only (no log file inside the container) — this is
the standard Docker pattern (docker logs, or point your log driver/aggregator at the
container) rather than something to volume-mount or rotate yourself.
All three images expose GET /health (used by the container HEALTHCHECK), matching
the JSON the admin dashboard's own health check reads.