# 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 # ` — 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