Files
selfpost/build/Dockerfile
T
mix ac5b37d1e2 panel: server status page, per-domain DNS checks, /domains move
Phase 13. Two new packages and one new screen.

internal/health owns the shared status vocabulary (ok/warn/error/unknown)
and the local checks: supervisord's process table, TLS certificate expiry
and the two milter sockets. Each check reports a problem as a status rather
than an error, so one broken component costs a line and not the page.

internal/dnscheck does the read-only lookups: forward-confirmed reverse DNS
for SELFPOST_HOSTNAME, and per-domain DKIM (compared against the key this
server actually signs with), SPF and DMARC. Every check is bounded by a
timeout and cached, and the resolver sits behind an interface so the tests
drive every branch without touching the network. The SPF check is
deliberately shallow: it looks for a mechanism literally covering the
server's address and does not follow include:/redirect=, so a record that
authorises us through an include is reported as "cannot tell" rather than
as a failure.

/status renders both, with the local checks in an HTMX-polled fragment and
the DNS lookups behind a Re-check button, and becomes the panel's landing
page: / now redirects there and the domain list lives at /domains. The
Reload button moves onto /status, where it reads as what it is — a
drift-recovery for the daemons — with text explaining what it regenerates.
A template test fails on any remaining href="/" so a stale link cannot
silently land on the wrong screen.

Also fixes a defect this made visible: the panel could never read the mail
queue in the documented deployment. postqueue relies on its setgid-postdrop
bit, which the compose file's no-new-privileges disables, so the Queue
screen always said "Could not read the mail queue" — including in the
released 1.0.0 image. The panel user is now a real member of postdrop,
which needs no setgid transition.

Verified in a container on the dev server against real DNS: PTR matching
(selfpost.mixfed.ru) and not matching (mixfed.ru), DKIM absent and
mismatched, SPF absent and via include:, DMARC p=quarantine/p=reject/absent,
and a resolver timeout degrading to "unknown" without hanging the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 22:04:37 +03:00

110 lines
4.7 KiB
Docker

# 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. go.sum arrived in Phase 2 with the
# SQLite driver and bcrypt.
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 codeberg.org/mix/selfpost/internal/buildinfo.Version=${VERSION}" \
-o /out/panel ./cmd/panel \
&& go build -trimpath \
-ldflags "-X codeberg.org/mix/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 in Phase 5.
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 \
&& 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
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
# 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"]