Files
webmail-client/docker/Dockerfile.backend
T
lukasandClaude Opus 5 95ca1b49f6 Antispam plugin (M7 phase C1): classify, file, and stay out of the way
Second-pass spam filtering per SPEC Appendix C. The engine is Rspamd in its own
container; this plugin is glue and policy, never a spam engine of its own.

Classification (§C.4) hooks message.new, acts only on inbox arrivals of enabled
accounts, and runs deny list -> allow list -> engine -> thresholds. Every
failure path ends in "deliver normally, unclassified": an unreachable engine, a
missing blob, an account with no Junk folder, a failed verdict write. Nothing
here may delay or lose mail, so no service declares depends_on: rspamd.

Two containers, started with everything else rather than behind a profile:
rspamd, and a redis of its own. The separate redis is not incidental — the app's
redis holds sync leases and arq queues under noeviction, and an ever-growing
Bayes corpus must never be able to fail a job enqueue. This one takes RDB
snapshots and evicts, because its data is derived and retrainable.

Network checks (RBL/SURBL/fuzzy) are off: they leak message-derived hashes and
URLs to third parties, which a self-hosted server's users did not ask for. The
engine makes no outbound connections in the default configuration.

The controller password is generated into .env like MASTER_KEY. It is internal;
humans will reach Rspamd through the admin panel, authenticated as the ordinary
administrator, rather than typing it.

local.d/groups.conf zeroes six SMTP-time symbols — DATE_IN_PAST,
PREVIOUSLY_DELIVERED, HFILTER_HOSTNAME_UNKNOWN, RCVD_NO_TLS_LAST, ARC_NA,
DMARC_NA — and greylisting is disabled. Measured against 398 real delivered
messages, those checks were firing on 44-71% of ordinary mail and pushing 11.3%
of it over the banner threshold; scoring mail at rest for facts true of every
delivered message is simply the wrong question. After: median 3.09 -> 0.70,
p95 7.00 -> 3.40, banner rate 11.3% -> 1.0%. The spec's 12.0/6.0 thresholds were
never the problem and are unchanged. Symbols are zeroed rather than removed so
verdicts stay explainable, and they matter again at the M6 edge (§C.7).

Still to come in C1: the §C.6 router and UI slots. C2 adds the learning loop —
until Bayes is trained, genuine scams and legitimate bulk mail overlap in the
5-8 band and no threshold separates them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
2026-09-16 22:14:43 +02:00

81 lines
2.9 KiB
Docker

# Production backend image: API, both worker roles, and the migration/admin CLI.
#
# One image, several commands (see docker/entrypoint.sh) — matches SPEC §6.3's
# WORKER_ROLE split. Build context is the REPO ROOT, because the image needs
# backend/ *and* plugins/ (the plugins are separate distributions that the
# loader discovers through the `webmail.plugins` entry-point group, SPEC §11).
#
# docker build -f docker/Dockerfile.backend -t webmail/backend .
# ---------- builder: resolve wheels into a self-contained venv ----------------
FROM python:3.11-slim AS builder
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
PYTHONDONTWRITEBYTECODE=1
# Only needed if a pinned dep has no manylinux wheel for this platform; the
# whole stage is discarded, so it costs nothing in the runtime image.
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Backend first: its dependency set is the slowest layer and changes least often.
COPY backend/pyproject.toml /src/backend/pyproject.toml
COPY backend/app /src/backend/app
COPY backend/workers /src/backend/workers
RUN pip install /src/backend
# Plugins are ordinary distributions installed on top (each owns its own deps).
COPY plugins /src/plugins
RUN pip install \
/src/plugins/webmail_plugin_calendar \
/src/plugins/webmail_plugin_contacts \
/src/plugins/webmail_plugin_labels \
/src/plugins/webmail_plugin_filters \
/src/plugins/webmail_plugin_antispam
# ---------- runtime -----------------------------------------------------------
FROM python:3.11-slim
ARG APP_VERSION=dev
ARG GIT_SHA=unknown
LABEL org.opencontainers.image.title="webmail-backend" \
org.opencontainers.image.version="${APP_VERSION}" \
org.opencontainers.image.revision="${GIT_SHA}"
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/opt/venv/bin:$PATH" \
APP_VERSION="${APP_VERSION}" \
GIT_SHA="${GIT_SHA}"
# tini reaps zombies and forwards SIGTERM, which the workers need for the <10 s
# graceful lease release in workers/run.py.
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --create-home --home-dir /home/webmail webmail
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
# Alembic's core tree lives outside the installed package, so it is copied in.
COPY backend/alembic.ini ./alembic.ini
COPY backend/migrations ./migrations
COPY backend/scripts ./scripts
COPY docker/entrypoint.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
# Blob store mount point (BLOB_ROOT). Ownership is set by the compose `init`
# service, which runs before anything else touches the bind mount.
RUN mkdir -p /data/blobs && chown webmail:webmail /data/blobs
VOLUME ["/data/blobs"]
USER webmail
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint"]
CMD ["api"]