Files
mailgoserver/docker-deploy/README.md
T

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.