Files
pl_mail/compose.override.yaml.dist
T
Paul LütznerandClaude Opus 5.5 a8364e34d2 feat(deploy): one worker container runs every background process
The stack was nine long-running containers: the web server, the
database, the Mercure hub, and six of plMail's own image, one per
background process. It is three now: php, worker and database, with
secrets-init still running once and exiting.

The worker runs `app:work`, a small supervisor keeping seven processes
alive the way Docker's restart policy kept seven containers alive: the
IMAP supervisor, one consumer per queue, the scheduler and the hub. A
clean exit (the hourly recycle, the restart signal) comes straight back;
a crash backs off from one second to thirty. Output passes through a
whole line at a time, so two processes' JSON records never interleave,
and SIGTERM reaches every child at once with 25s to finish. Each queue
keeps a process of its own, which is the part the split was for: a
consumer inside a long handler cannot take a send. And each process
keeps its container's old name as APP_CONTAINER_NAME, so heartbeats,
/healthz and the log filters are unchanged.

The hub is the official Mercure 1.x binary, copied into the image and
run as one of those processes. Not FrankenPHP's built-in hub, which is
0.24 and refuses the RFC 9068 tokens plMail has minted since v0.2.31.
The worker answers to `mercure` on the network, so the Caddyfile proxy
and MERCURE_URL reach it where they reached the hub container, and older
compose files keep working with this image unchanged. It is configured
from the application's own settings, including MERCURE_COOKIE_NAME, so a
plain-HTTP install names the cookie once for both halves. A 5s shutdown
grace stops it waiting on open subscriptions, which made every stop take
the whole 25s.

The dev override loses its per-worker blocks, one of which (worker-bulk)
had been missing, so that queue ran the published image's code in
development. The demo overlay runs app:work --without=imap-supervisor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:20:31 +02:00

110 lines
4.2 KiB
Plaintext

# Every app service builds the same dev stage, so they share one image — one
# build, whichever of them starts it.
#
# The `image` line is the load-bearing part. compose.yaml names these services
# `ghcr.io/karatektus/pl_mail:latest`, and compose tags what it builds with the
# service's image name — so without this, every `docker compose up --build`
# overwrites the published tag locally with a dev image. That image carries no
# application code (the source arrives via the bind mount below), which is
# correct for dev and fatal anywhere else: if this override ever goes missing,
# compose finds that local tag, does not pull, and boots an app-less container
# that falls into the entrypoint's symfony/skeleton branch and crash-loops.
# Building under our own name leaves the published tag alone.
x-dev-build: &dev-build
image: pl_mail-dev:latest
build:
context: .
target: frankenphp_dev
services:
# Builds locally like every other app service. Without this it is the one
# service still pulling ghcr.io/karatektus/pl_mail, which would run emulated
# on an arm64 host — and since everything else waits on it completing
# successfully, that one container gates the whole stack.
secrets-init:
<<: *dev-build
php:
<<: *dev-build
volumes:
- ./:/app
- app_var:/app/var
# Durable mail data must be visible to BOTH the web container (which
# serves downloads) and the workers (which write during sync). php
# overlays /app/var with the private app_var volume to keep cache/logs
# off the bind mount, which otherwise hides these two directories from
# everything else.
- app_attachments:/app/var/attachments
- app_raw:/app/var/raw
- app_uploads:/app/var/uploads
- ./frankenphp/Caddyfile:/etc/frankenphp/Caddyfile:ro
- ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/conf.d/20-app.dev.ini:ro
environment:
APP_ENV: ${APP_ENV:-dev}
XDEBUG_MODE: "${XDEBUG_MODE:-off}"
extra_hosts:
- host.docker.internal:host-gateway
tty: true
# Everything that is not the web server (app:work), from the dev image with
# the source mounted. A service the base file defines and this one does not
# still starts, from the published image and without the source: not a
# missing container but one running yesterday's code. There used to be one
# block per worker here, and worker-bulk's had been forgotten; one block has
# nothing left to forget.
worker:
<<: *dev-build
volumes:
- ./:/app
# Durable mail data must be visible to BOTH the web container (which
# serves downloads) and the workers (which write during sync). php
# overlays /app/var with the private app_var volume to keep cache/logs
# off the bind mount, which otherwise hides these directories from
# everything else.
- app_attachments:/app/var/attachments
- app_raw:/app/var/raw
- app_uploads:/app/var/uploads
environment:
APP_ENV: ${APP_ENV:-dev}
database:
ports:
- "5432:5432"
adminer:
image: adminer
restart: unless-stopped
ports:
- "8080:8080"
environment:
ADMINER_DEFAULT_SERVER: database
tailwind:
<<: *dev-build
volumes:
- .:/app
# Runs bin/console, so it goes through the app entrypoint and needs the
# same secrets every other service uses. Without this it would generate
# its own and refuse to start.
- app_secrets:/app/var/secrets
- app_var:/app/var
command: php bin/console tailwind:build --watch
# The image healthcheck probes the FrankenPHP admin endpoint on :2019, but
# this service replaces the server with the Tailwind watcher, so nothing
# ever listens there and the container is reported unhealthy while working
# fine. Same reason the worker disables it in compose.yaml.
healthcheck:
disable: true
depends_on:
- php
# No `mercure` block: the hub is a process in the worker now, reachable
# through php at /.well-known/mercure like everything else. The Mercure
# bundle's recipe put one here to publish the hub's port; brought back, it
# would define a service with no image and compose would refuse the file.
volumes:
app_var:
app_attachments:
app_raw:
app_uploads: