# syntax=docker/dockerfile:1
#
# SelfPost — single Debian-slim image running postfix + opendkim + panel under
# supervisord. Build from the repository root:
#
#     docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .

# ---- build stage -------------------------------------------------------------
FROM golang:1.26-trixie AS build

WORKDIR /src

# Version stamped into both binaries; MUST match the image tag.
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:trixie-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.
# (postfix 3.10.13-0+deb13u1, debian/templates:14 and :50:
# postfix/main_mailer_type and postfix/mailname are the questions the install
# asks; debian/postfix.postinst:156-171 writes main.cf for any answer but "No
# configuration".)
# openssl is named because postfix-cert-reload.sh runs it, not left to arrive
# with ca-certificates. python3 arrives with supervisor (supervisor 4.2.5-3,
# debian/control:29, ${python3:Depends}) - crashexit.py. sasl2-bin brings
# saslpasswd2 (cyrus-sasl2 2.1.28+dfsg1-9, debian/rules:207,
# debian/sasl2-bin.install:3, usr/sbin) and depends on db-util
# (debian/control:36); libsasl2-modules brings the PLAIN and LOGIN plugins
# (debian/libsasl2-modules.install:4 and :6); the sasldb auxprop plugin is in
# libsasl2-modules-db (debian/libsasl2-modules-db.install:1), a Depends of
# libsasl2-2 (debian/control:65). db-util is named for /usr/bin/db_dump, which
# the panel runs (internal/app/sasl.go); that db-util ships that path on
# trixie is not in a source read here - the image build and the e2e sasldb
# test say whether it does.
# opendkim-tools is not installed: nothing in the image runs its programs
# (DKIM keys are made in Go, internal/domain/dkim.go).
# systemd-standalone-sysusers is named so that cron-daemon-common, which
# logrotate brings (logrotate 3.22.0-1, debian/control: "Depends: cron |
# anacron | cron-daemon | systemd-sysv"), creates its group with it rather
# than with the whole of systemd, the first alternative of its "Depends:
# systemd | systemd-standalone-sysusers | systemd-sysusers" (dpkg -s in the
# image, 2026-10-15). Nothing here runs systemd.
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 \
      sasl2-bin \
      libsasl2-modules \
      db-util \
      supervisor \
      logrotate \
      ca-certificates \
      openssl \
      curl \
      systemd-standalone-sysusers \
 && rm -rf /var/lib/apt/lists/*

# Debian's postfix runs its chroot sync on every "postfix check" and "postfix
# start-fg" (postfix 3.10.13-0+deb13u1, debian/patches/
# debian-run-configure-instance-from-create-missing.patch); with no service
# chrooted (build/postfix-config.sh) it would only copy /etc/resolv.conf and
# the like into the queue under /data on each start. SYNC_CHROOT empty turns
# it off (debian/configure-instance.in:22-24 and :44; debian/README.Debian:136-138).
RUN printf 'SYNC_CHROOT=\n' > /etc/default/postfix

# Unprivileged user for the panel process. Its ids are left to useradd: the
# entrypoint gives /data back to it by name on every start, so a number
# that differs from one image to the next changes nothing.
RUN useradd --system --no-create-home --shell /usr/sbin/nologin panel

# Shared group bridging the unprivileged services: 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. (The supervisor control socket has a group of
# its own, `supervisor`, below.)
# The id is fixed, because the host sees it: an operator gives the TLS
# private key on the host to this group by number, so that the panel can put
# it into a full backup (guide § Taking a full backup). It lies outside the
# range the distribution hands out to system groups, 100-999 (adduser 3.152,
# /etc/adduser.conf: "Default: FIRST_SYSTEM_GID=100, LAST_SYSTEM_GID=999";
# passwd 1:4.17.4-2, /etc/login.defs: SYS_GID_MIN 101, SYS_GID_MAX 999 - both
# read in the image, 2026-10-15), so no package can take it, and far above
# the 1000 a host starts its users' groups at, so a host is unlikely to have
# a group of that number that would read the key too. Until 2.7.0 it was
# 998, inside that range: in the trixie image systemd took 998 for
# systemd-network and the build failed, and on the test server's host 998
# is systemd-network too (seen 2026-10-15).
# groupadd(8) refuses a --gid already in use, so an id taken after all would
# fail this build rather than share the group.
RUN groupadd --system --gid 10998 selfpost \
 && usermod -aG selfpost panel \
 && usermod -aG selfpost opendkim \
 && usermod -aG selfpost postfix

# The panel reads the mail queue with `postqueue -p` (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. The same membership is what lets the panel submit its
# own notifications with sendmail(1) - postdrop, too, is setgid and would fail
# to write the maildrop directory (internal/notify/mailer.go). It does not let
# the panel bypass any Postfix restriction that a local user does not already
# have through the world-executable sendmail.
# (Postfix 3.10.13, conf/postfix-files:140-141: postdrop and postqueue are
# root:$setgid_group 2755; Debian sets them root:postdrop 02555, postfix
# 3.10.13-0+deb13u1, debian/postfix.postinst:134-141. Linux,
# Documentation/userspace-api/no_new_privs.rst:
# with no_new_privs, execve() no longer changes the gid for a setgid file.)
RUN usermod -aG postdrop panel
# The supervisord control socket's group (supervisord.conf, [unix_http_server]):
# the panel alone - not OpenDKIM's or Postfix's users, which share `selfpost`
# for the files under /data.
RUN groupadd --system supervisor \
 && usermod -aG supervisor 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. OFL.txt is the SIL
# Open Font License 1.1 that must travel with the IBM Plex WOFF2 files
# embedded in the panel binary (NOTICE).
COPY --chmod=0644 LICENSE NOTICE /usr/share/doc/selfpost/
COPY --chmod=0644 internal/web/view/static/OFL.txt /usr/share/doc/selfpost/OFL.txt

# File modes are pinned here so a build context copied from a checkout without
# POSIX permissions (e.g. Windows tar sync) cannot land group-writable config
# that logrotate would silently ignore — see docs/development.md § Building
# binaries and the image.
COPY --chmod=0644 build/opendkim.conf /etc/opendkim.conf
COPY --chmod=0644 build/logrotate-mail.conf /etc/logrotate.d/mail
COPY --chmod=0755 build/postfix-wrapper.sh /usr/local/bin/postfix-wrapper.sh
COPY --chmod=0755 build/postfix-reload.sh /usr/local/bin/postfix-reload.sh
COPY --chmod=0755 build/postfix-config.sh /usr/local/bin/postfix-config.sh
COPY --chmod=0755 build/postfix-cert-reload.sh /usr/local/bin/postfix-cert-reload.sh
COPY --chmod=0755 build/logrotate-loop.sh /usr/local/bin/logrotate-loop.sh
COPY --chmod=0755 build/crashexit.py /usr/local/bin/crashexit.py
COPY --chmod=0755 build/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chmod=0644 build/supervisord.conf /etc/supervisor/supervisord.conf

# Published submission ports: 465 (smtps, primary) and 587 (submission, optional),
# inbound SMTP on 25 when INBOUND_RELAY_ENABLE=true, plus the panel on 8080.
# Outbound delivery dials remote MXs on 25 as a client even when inbound is off.
EXPOSE 8080 465 587 25

# 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.
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
