Files
photoanalyzer/docker-compose.yml

106 lines
4.7 KiB
YAML

# The deployed runtime (US08-03): one API, one worker, one library, one volume.
#
# The same image (US08-02) runs both roles, so what is composed here is process
# topology, not a second application. Three invariants shape it:
#
# * one writer — `serve` and `worker` each take the library process lock for their
# role (US07-05), and both containers mount the *same* data volume, which is what
# makes the lock file visible to both. Scaling `worker` past 1 is refused by that
# lock rather than by anyone remembering not to;
# * one local filesystem — the database, its write-ahead log, the thumbnail cache,
# and the backups live in the `data` volume, and SQLite in WAL mode requires real
# local-filesystem locking. A network mount (NFS, SMB, or a cloud volume driver)
# is unsupported for it; that is a corrupted database, not a slow one;
# * one library path vocabulary — `PHOTO_PIPELINE_LIBRARY_ROOTS` names the path
# *inside* the container, which is also the bind mount's target below. A root
# that is not mounted there makes `serve` and `worker` refuse at startup instead
# of writing into the container's throwaway layer.
#
# Configuration and secrets come from the environment only: copy `.env.example` to
# `.env` and fill it in. Nothing is baked into the image and nothing with a value in
# it is committed.
#
# cp .env.example .env && $EDITOR .env
# docker compose up -d --build
#
# Operating it is operating the same CLI — `docker compose run --rm --no-deps api
# <command>` — see README, "Composed runtime".
name: photo-pipeline
x-runtime: &runtime
image: ${PHOTO_PIPELINE_IMAGE:-photo-pipeline:dev}
build:
context: .
args:
# Everything the app renames or rewrites has to stay owned by the host user
# the library already belongs to.
UID: ${PHOTO_PIPELINE_UID:-1000}
GID: ${PHOTO_PIPELINE_GID:-1000}
user: "${PHOTO_PIPELINE_UID:-1000}:${PHOTO_PIPELINE_GID:-1000}"
env_file:
- ${PHOTO_PIPELINE_ENV_FILE:-.env}
volumes:
- data:/data
- "${PHOTO_PIPELINE_LIBRARY_HOST_PATH:?set PHOTO_PIPELINE_LIBRARY_HOST_PATH to the photo library on this host}:${PHOTO_PIPELINE_LIBRARY_ROOTS:?set PHOTO_PIPELINE_LIBRARY_ROOTS to the container-side library path}"
# Jobs check for cancellation between items and leave a resumable record; a
# too-short grace period turns an orderly stop into a recovery on next start.
stop_grace_period: 30s
x-environment: &environment
# Set here rather than left to the file: these two are what the composition itself
# promises, and an `.env` that disagreed would move the database off the volume or
# the library off its mount.
PHOTO_PIPELINE_DATA_DIR: /data
PHOTO_PIPELINE_LIBRARY_ROOTS: ${PHOTO_PIPELINE_LIBRARY_ROOTS}
services:
# Migrations run to completion before either role accepts work, through the same
# backup-then-migrate path the roles use (US07-05): a pending upgrade is snapshotted
# first, and a failed one exits non-zero with the backup named — so `api` and
# `worker` never start, and the previous database is left intact and restorable.
migrate:
<<: *runtime
command: ["migrate"]
environment: *environment
restart: "no"
api:
<<: *runtime
command: ["serve"]
environment:
<<: *environment
# Published to host loopback below. Inside the container the server must bind
# the container's own interface for that publish to reach it, which is exactly
# what makes the access secret mandatory (US08-01) — `serve` refuses to start
# without one. Exposing the port beyond loopback additionally needs
# PHOTO_PIPELINE_ALLOWED_HOSTS to name the hostname it is reached under.
PHOTO_PIPELINE_HOST: 0.0.0.0
PHOTO_PIPELINE_PORT: 8000
ports:
# Host side only: PHOTO_PIPELINE_PORT in `.env` moves the *published* port, and
# the container always serves 8000, which is what the image's health check probes.
- "${PHOTO_PIPELINE_PUBLISH_ADDRESS:-127.0.0.1}:${PHOTO_PIPELINE_PORT:-8000}:8000"
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
# One worker. A second one is refused by the library lock in the shared data
# volume, which is the point: `docker compose up --scale worker=2` fails loudly
# instead of running two writers against one library.
worker:
<<: *runtime
command: ["worker", "--id", "worker-1"]
environment: *environment
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
volumes:
# Local driver on purpose: the database, WAL, thumbnail cache, and backups need a
# real local filesystem. Do not point this at NFS, SMB, or a network volume driver.
data:
driver: local