Deploy: a release that adds a service must actually start it
Three things stood between v1.1.0 and a working anti-spam engine on an existing install, each silent: `update` rolled only worker-sync, worker-jobs, api and web. A release that adds a service — this one adds two — would report success while never creating it. It now converges the rest with `up -d --no-recreate`, which creates what is absent and leaves the rolled services alone. .env.prod.example pinned ENABLED_PLUGINS to the four plugins that existed when it was written. The entrypoint's default includes antispam, but an explicit value in .env wins, so migrate would skip the plugin's tables and every antispam request would fail at runtime rather than at install. The template is fixed for new installs; `doctor` now warns existing ones, comparing .env against the entrypoint's default so there is still one list, not two. And the runbook now documents what the engine costs (~150-300 MB), that nothing depends on it by design, that `init` must be re-run for the controller password, and that its Bayes database is derived data like the search index. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
@@ -328,6 +328,26 @@ cmd_doctor() {
|
||||
else grn " ${avail} MB free on ${DATA_ROOT}"; fi
|
||||
}
|
||||
|
||||
# A plugin shipped in the image but absent from ENABLED_PLUGINS never gets its
|
||||
# tables created, and then fails at runtime rather than at install. The
|
||||
# entrypoint's default is the list of what ships, so compare against that
|
||||
# instead of duplicating it here.
|
||||
local shipped missing=""
|
||||
shipped="$(sed -n 's/^: "${ENABLED_PLUGINS:=\(.*\)}"/\1/p' "${ROOT_DIR}/docker/entrypoint.sh")"
|
||||
if [ -n "${ENABLED_PLUGINS:-}" ] && [ -n "$shipped" ]; then
|
||||
local p
|
||||
for p in $shipped; do
|
||||
case " ${ENABLED_PLUGINS} " in *" $p "*) ;; *) missing="${missing} $p" ;; esac
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
ylw " ENABLED_PLUGINS is missing:${missing}"
|
||||
echo " these ship in the image but will have no tables created."
|
||||
echo " fix: add them to ENABLED_PLUGINS in ${ENV_FILE}, then: webmail.sh update"
|
||||
else
|
||||
grn " ENABLED_PLUGINS covers every plugin in the image"
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
local p
|
||||
for p in "WEB_PORT:${WEB_PORT:-8088}" "ADMIN_PORT:${ADMIN_PORT:-8089}"; do
|
||||
@@ -479,6 +499,11 @@ cmd_update() {
|
||||
# then the API, then the web front end.
|
||||
IMAGE_TAG="$target" compose up -d worker-sync worker-jobs
|
||||
IMAGE_TAG="$target" compose up -d api web
|
||||
# Converge anything the ordered roll does not name — a release that adds a
|
||||
# service (the antispam engine did) would otherwise never start it, and the
|
||||
# stack would look updated while missing a container. --no-recreate so this
|
||||
# only creates what is absent and leaves the rolled services alone.
|
||||
IMAGE_TAG="$target" compose up -d --no-recreate
|
||||
|
||||
echo "== 5/5 health gate (${HEALTH_TIMEOUT}s)"
|
||||
if health_gate; then
|
||||
|
||||
@@ -97,7 +97,7 @@ OAUTH_MS_CLIENT_SECRET=
|
||||
|
||||
# Plugins whose schema is migrated on every start. Remove one to stop shipping
|
||||
# its tables; `hello` is the SDK demo and is intentionally not listed.
|
||||
ENABLED_PLUGINS="calendar contacts labels filters"
|
||||
ENABLED_PLUGINS="calendar contacts labels filters antispam"
|
||||
|
||||
# --- Backups ----------------------------------------------------------------
|
||||
BACKUP_AT=03:30
|
||||
|
||||
+19
-1
@@ -556,12 +556,30 @@ Steady state for one or two mail accounts, `SEARCH_BACKEND=pg`:
|
||||
| worker-sync | 120-250 MB | `WORKER_SYNC_MEM_LIMIT=512m` |
|
||||
| worker-jobs | 150-350 MB | `WORKER_JOBS_MEM_LIMIT=640m` |
|
||||
| redis / web / backup | < 100 MB total | — |
|
||||
| **total** | **~0.7-1.2 GB** | ~2.2 GB of limits |
|
||||
| rspamd + its redis | 150-300 MB | `RSPAMD_MEM_LIMIT=256m`, `RSPAMD_REDIS_MEM_LIMIT=160m` |
|
||||
| **total** | **~0.9-1.5 GB** | ~2.6 GB of limits |
|
||||
| meilisearch (optional) | 300-800 MB | `MEILI_MEM_LIMIT=1g` |
|
||||
|
||||
On a 2 GB NAS: stay on `SEARCH_BACKEND=pg` and drop `PG_SHARED_BUFFERS` to
|
||||
`128MB`. Tight but workable. On 4 GB+ you have room for Meilisearch.
|
||||
|
||||
### The anti-spam engine
|
||||
|
||||
`rspamd` and `redis-rspamd` start with everything else — spam filtering is part
|
||||
of the stack, not an add-on. Two things follow from that:
|
||||
|
||||
- **Nothing waits for them.** No service declares `depends_on: rspamd`, so an
|
||||
engine that is down, slow or restarting cannot delay mail. Classification is
|
||||
best-effort; delivery is not. The health gate ignores it for the same reason.
|
||||
- **`webmail.sh init` generates `RSPAMD_CONTROLLER_PASSWORD`.** It is internal —
|
||||
nobody types it — but the engine will not start without it. Upgrading an
|
||||
install that predates the engine means re-running `init`, which only fills in
|
||||
values that are empty and never touches `MASTER_KEY`.
|
||||
|
||||
Its Bayes database lives in `$DATA_ROOT/rspamd-redis`. Like the search index,
|
||||
that is derived data: it rides along in backups but never needs restoring, and
|
||||
retraining rebuilds it.
|
||||
|
||||
Two NAS-specific realities worth knowing:
|
||||
|
||||
- **Disk hibernation won't happen.** Active accounts hold IMAP IDLE connections
|
||||
|
||||
Reference in New Issue
Block a user