# syntax=docker/dockerfile:1 # # SelfPost — single Debian-slim image running postfix + opendkim + panel under # supervisord (spec 4). Build from the repository root: # # docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev . # ---- build stage ------------------------------------------------------------- FROM golang:1.26-bookworm AS build WORKDIR /src # Version stamped into both binaries; MUST match the image tag (spec 7.5.A). ARG VERSION=dev # Module metadata first for layer caching. COPY go.mod go.sum ./ RUN go mod download COPY cmd ./cmd COPY internal ./internal ENV CGO_ENABLED=0 RUN go vet ./... \ && go build -trimpath \ -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=${VERSION}" \ -o /out/panel ./cmd/panel \ && go build -trimpath \ -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=${VERSION}" \ -o /out/selfpost-backup ./cmd/selfpost-backup # ---- runtime stage ----------------------------------------------------------- FROM debian:bookworm-slim AS runtime ENV DEBIAN_FRONTEND=noninteractive # Preseed Postfix so its install is non-interactive and yields a working # main.cf. The real relay configuration is generated by the panel. RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \ && echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \ && apt-get update \ && apt-get install -y --no-install-recommends \ postfix \ opendkim \ opendkim-tools \ sasl2-bin \ libsasl2-modules \ db-util \ supervisor \ logrotate \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* # Unprivileged user for the panel process (spec 7.6.8). RUN useradd --system --no-create-home --shell /usr/sbin/nologin panel # Shared group bridging the unprivileged services (spec 5.1, 6): the panel # generates per-domain DKIM keys, application SASL accounts (sasldb2) and the # Postfix sender map, while OpenDKIM and Postfix (different users) must read # them. Membership in this group — plus setgid dirs under /data (set up in # entrypoint.sh) — lets OpenDKIM read the panel-owned keys and lets Postfix read # the sasldb2/sender map, and lets the panel reach the supervisor control socket # to signal OpenDKIM/Postfix reloads without any process running as root. RUN groupadd --system selfpost \ && usermod -aG selfpost panel \ && usermod -aG selfpost opendkim \ && usermod -aG selfpost postfix # The panel reads the mail queue with `postqueue -p` (spec 7.2.11, and the # status page's queue card). postqueue is setgid postdrop, which normally gives # it the group needed to reach Postfix's showq socket — but the documented # deployment runs with `no-new-privileges`, which disables setgid transitions, # so the panel would always see "Permission denied". Making `panel` a real # member of postdrop grants the same access without relying on a setgid # escalation the hardening deliberately forbids. postdrop membership is # read-side only: it does not let the panel bypass any Postfix restriction that # a local user does not already have through the world-executable sendmail. RUN usermod -aG postdrop panel # Runtime directories: milter sockets and the consolidated persistent root. RUN mkdir -p /run/opendkim /run/selfpost /data \ && chown opendkim:opendkim /run/opendkim \ && chown panel:panel /run/selfpost /data COPY --from=build /out/panel /usr/local/bin/panel COPY --from=build /out/selfpost-backup /usr/local/bin/selfpost-backup # Licence text shipped with the image (AGPL-3.0 conveyance). The panel also # serves the same text at /license from an embedded copy. COPY LICENSE NOTICE /usr/share/doc/selfpost/ COPY build/opendkim.conf /etc/opendkim.conf COPY build/logrotate-mail.conf /etc/logrotate.d/mail COPY build/postfix-wrapper.sh /usr/local/bin/postfix-wrapper.sh COPY build/postfix-config.sh /usr/local/bin/postfix-config.sh COPY build/postfix-cert-reload.sh /usr/local/bin/postfix-cert-reload.sh COPY build/logrotate-loop.sh /usr/local/bin/logrotate-loop.sh COPY build/crashexit.py /usr/local/bin/crashexit.py COPY build/entrypoint.sh /usr/local/bin/entrypoint.sh COPY build/supervisord.conf /etc/supervisor/supervisord.conf RUN chmod +x /usr/local/bin/postfix-wrapper.sh /usr/local/bin/postfix-config.sh \ /usr/local/bin/postfix-cert-reload.sh /usr/local/bin/logrotate-loop.sh \ /usr/local/bin/crashexit.py /usr/local/bin/entrypoint.sh # Published submission ports: 465 (smtps, primary) and 587 (submission, optional) # plus the panel on 8080. Outbound delivery dials remote MXs on 25 as a client, # which needs no inbound listener or EXPOSE. EXPOSE 8080 465 587 # Liveness probe: panel HTTP plus mail-path processes (opendkim, panel, postfix). # Does not verify TLS, DNS, or end-to-end delivery — see docs/guide.md Operations. HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ CMD curl -fsS http://127.0.0.1:8080/healthz || exit 1 # The entrypoint fixes /data ownership (bind mount) as root, then execs # supervisord, which becomes PID 1 and owns process supervision (spec 4). ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]