add docker setup

This commit is contained in:
2026-08-15 06:22:02 +01:00
parent 892f366a16
commit 310700407e
11 changed files with 410 additions and 19 deletions
+9
View File
@@ -0,0 +1,9 @@
# Copy to .env (in this docker-deploy/ folder) and adjust if the defaults below clash
# with something else on the host, or if you want to run both the "standalone" and
# "with-rspamd" profiles side by side (give each its own set of host ports).
SMTP_PORT=25
SMTP_TLS_PORT=465
IMAP_PORT=143
IMAP_TLS_PORT=993
WEB_HTTP_PORT=5000
WEB_HTTPS_PORT=5001
+50
View File
@@ -0,0 +1,50 @@
# syntax=docker/dockerfile:1
#
# Standalone mailgoserver image — SMTP + IMAP + admin dashboard + webmail, no rspamd.
# The built-in heuristic spam score (internal/mailstore/spam.go) always runs regardless;
# this is for anyone who doesn't want rspamd's extra dependency. See Dockerfile.rspamd
# for the bundled variant.
#
# Build from the repo root:
# docker build -f docker-deploy/Dockerfile -t mailgoserver .
FROM golang:1.26-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# modernc.org/sqlite and every other dependency here are pure Go (no cgo), so a fully
# static binary is just CGO_ENABLED=0 — no libc/gcc needed in the runtime image.
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mailgoserver .
FROM debian:bookworm-slim
# ca-certificates: outbound direct-to-MX delivery verifies remote STARTTLS certs.
# tzdata: [Server] time_zone (e.g. "Europe/London") needs the real IANA zone
# database — time.LoadLocation silently falls back to UTC without it.
# curl: used by the HEALTHCHECK below.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates tzdata curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/mailgoserver /usr/local/bin/mailgoserver
# Everything persistent — the auto-generated settings.ini, the SQLite DB, encrypted
# mailbox storage, DKIM/mailstore master keys, TLS certs, the app secret — lives under
# whatever directory the process is started from (see config.Load / main.go's
# os.Getwd()). One volume here covers all of it; no need to enumerate subpaths.
WORKDIR /app/data
VOLUME ["/app/data"]
# Defaults from internal/config/config.go's generated settings.ini: SMTP 25, direct-TLS
# SMTP 465, IMAP 143, direct-TLS IMAP 993 (the actual standard ports — binding them
# needs no setcap/capability here since this container runs as root), admin/webmail
# HTTP 5000, HTTPS 5001 (deliberately non-privileged; put a reverse proxy or the host's
# own 80/443 in front if you want those too).
EXPOSE 25 465 143 993 5000 5001
# --host 0.0.0.0 is required: the binary's own default is 127.0.0.1, which would only
# be reachable from inside this container, never through a published port.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -fs http://127.0.0.1:5000/health || exit 1
ENTRYPOINT ["mailgoserver", "--host", "0.0.0.0"]
+47
View File
@@ -0,0 +1,47 @@
# syntax=docker/dockerfile:1
#
# mailgoserver + the latest rspamd, bundled in one container. Both processes share this
# container's network namespace, so rspamd's default "normal" worker
# (127.0.0.1:11333, the /checkv2 scanning API — see internal/mailstore/rspamd.go) is
# already exactly what [Rspamd] url defaults to in settings.ini. Just set
# [Rspamd] enabled = true after first boot (see docker-deploy/README.md) and restart.
#
# Build from the repo root:
# docker build -f docker-deploy/Dockerfile.rspamd -t mailgoserver-rspamd .
FROM golang:1.26-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mailgoserver .
FROM debian:bookworm-slim
# rspamd is installed from its own APT repo (rspamd.com), not Debian's bundled
# package, which tends to lag several releases behind — this is genuinely the latest
# stable release, matching what was asked for.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates tzdata curl gnupg lsb-release \
&& mkdir -p /usr/share/keyrings \
&& curl -fsSL https://rspamd.com/apt-stable/gpg.key | gpg --dearmor -o /usr/share/keyrings/rspamd.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/rspamd.gpg] https://rspamd.com/apt-stable/ $(lsb_release -cs) main" \
> /etc/apt/sources.list.d/rspamd.list \
&& apt-get update && apt-get install -y --no-install-recommends rspamd \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/mailgoserver /usr/local/bin/mailgoserver
COPY docker-deploy/entrypoint-rspamd.sh /usr/local/bin/entrypoint-rspamd.sh
RUN chmod +x /usr/local/bin/entrypoint-rspamd.sh
# /app/data: same as the standalone image (settings.ini, DB, mailstore, keys, certs).
# /var/lib/rspamd: rspamd's own Bayes/fuzzy-hash storage, kept persistent separately so
# rebuilding the image doesn't reset spam-learning state.
WORKDIR /app/data
VOLUME ["/app/data", "/var/lib/rspamd"]
EXPOSE 25 465 143 993 5000 5001
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD curl -fs http://127.0.0.1:5000/health || exit 1
ENTRYPOINT ["/usr/local/bin/entrypoint-rspamd.sh"]
+98
View File
@@ -0,0 +1,98 @@
# Docker deployment
Two independent images, both built from the same source tree:
- **`Dockerfile`** — mailgoserver only.
- **`Dockerfile.rspamd`** — mailgoserver + the latest [rspamd](https://rspamd.com) in the
*same* container, already wired together (see below). Use this one if you want
stronger spam filtering than the built-in heuristic score alone.
`docker-compose.yml` defines both as Compose **profiles** so a plain `docker compose up`
can't accidentally start both at once:
```bash
cd docker-deploy
# mailserver only
docker compose --profile standalone up -d --build
# mailserver + rspamd, bundled
docker compose --profile with-rspamd up -d --build
```
Either way, the app itself now 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. Binding the
low mail ports needs no extra capability here since the container runs as root. 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 both
profiles side by side.
## What happens on first boot
There's no baked-in config. On first start, the binary generates a fresh
`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
`/app/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](../README.md) 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`:
```bash
docker exec -it mailgoserver sh -c 'vi /app/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 (the `with-rspamd` profile)
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:
```ini
[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).
This bundle intentionally skips Redis — rspamd runs fine without it for SPF/DKIM/RBL/
regexp-based scoring, but Bayes learning and greylisting need it. Add a `redis` service
to `docker-compose.yml` and point rspamd's `redis.conf` at it if you need those.
## Persistence
| Volume | What's in it |
|---|---|
| `mailserver-data` / `mailserver-rspamd-data` | `settings.ini`, the SQLite DB, encrypted mailbox storage, DKIM/mailstore master keys, TLS certs, the CSRF app secret — everything mailgoserver itself owns. |
| `rspamd-data` (rspamd profile only) | rspamd's own Bayes/fuzzy-hash storage, so spam-learning state survives image rebuilds. |
Back up the `*-data` 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.
## Logs / health
```bash
docker compose --profile standalone logs -f # or --profile with-rspamd
docker inspect --format '{{.State.Health.Status}}' mailgoserver
```
Both images expose `GET /health` (used by the container `HEALTHCHECK`), matching the
JSON the admin dashboard's own health check reads.
+51
View File
@@ -0,0 +1,51 @@
# Two independent setups, selected with --profile so they don't both start by accident:
#
# docker compose --profile standalone up -d --build # mailserver only
# docker compose --profile with-rspamd up -d --build # mailserver + rspamd, same container
#
# Both default to the standard mail ports on the host (25/465/143/993) — the app itself
# now binds those directly, no port remapping needed — plus the app's own non-privileged
# web ports (5000/5001; put a reverse proxy or your own 80/443 mapping in front of those
# if you want the dashboard on standard web ports too). Only run one profile at a time
# unless you've overridden the host ports for one of them (see .env.example) — they'd
# otherwise both try to bind the same host ports.
services:
mailserver:
build:
context: ..
dockerfile: docker-deploy/Dockerfile
container_name: mailgoserver
restart: unless-stopped
profiles: ["standalone"]
ports:
- "${SMTP_PORT:-25}:25"
- "${SMTP_TLS_PORT:-465}:465"
- "${IMAP_PORT:-143}:143"
- "${IMAP_TLS_PORT:-993}:993"
- "${WEB_HTTP_PORT:-5000}:5000"
- "${WEB_HTTPS_PORT:-5001}:5001"
volumes:
- mailserver-data:/app/data
mailserver-rspamd:
build:
context: ..
dockerfile: docker-deploy/Dockerfile.rspamd
container_name: mailgoserver-rspamd
restart: unless-stopped
profiles: ["with-rspamd"]
ports:
- "${SMTP_PORT:-25}:25"
- "${SMTP_TLS_PORT:-465}:465"
- "${IMAP_PORT:-143}:143"
- "${IMAP_TLS_PORT:-993}:993"
- "${WEB_HTTP_PORT:-5000}:5000"
- "${WEB_HTTPS_PORT:-5001}:5001"
volumes:
- mailserver-rspamd-data:/app/data
- rspamd-data:/var/lib/rspamd
volumes:
mailserver-data:
mailserver-rspamd-data:
rspamd-data:
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
# Starts rspamd in the background, then hands off to mailgoserver as the container's
# main (foreground) process.
#
# ponytail: no supervisor / restart-on-crash for rspamd here — if it dies mid-run, mail
# keeps flowing without the extra scoring rather than the container crashing (the
# built-in heuristic score in internal/mailstore/spam.go always runs regardless, and
# CheckRspamd swallows connection errors rather than rejecting mail over them — see
# internal/mailstore/rspamd.go). Add s6-overlay or supervisord if unattended rspamd
# uptime matters more than that.
rspamd -f &
# Give rspamd's normal worker a moment to bind :11333 before the first message can
# possibly arrive — purely cosmetic, mailgoserver degrades gracefully either way.
sleep 1
exec mailgoserver --host 0.0.0.0 "$@"