# One image, two roles (US08-02). # # The application is not self-contained Python: it shells out to `exiftool` for every # EXIF checkpoint and to `immich-go` for every upload, and it serves the static # frontend from `frontend/`. All three are installed here at pinned versions, because # an image whose external tools drift is an image whose metadata checkpoints and # upload reports drift with them (concept §15, "External integration risks"). # # Everything is pinned: # * the base image by tag *and* digest, so a moved tag cannot change the runtime; # * exiftool by its Debian package version, verified against `exiftool -ver`; # * immich-go by release version and per-architecture SHA-256 of the release asset. # The verified versions become image labels and /etc/photo-pipeline/versions.json, # which `python -m photo_pipeline diagnostics` reports — so a running container can # prove what it contains instead of being trusted about it. # # The project is installed editable on purpose: `photo_pipeline.db` resolves # `alembic.ini` and `migrations/`, and the API resolves `frontend/`, relative to the # repository root. An editable install keeps that one layout instead of scattering the # same files across site-packages and a source tree. ARG PYTHON_IMAGE=python:3.12.14-slim-trixie@sha256:2c941e860699f878900b0edc2403613c234d4b32eda3cc9fa7036991a2a63c4a # ── the uploader, fetched and verified outside the final layer ──────────────── FROM ${PYTHON_IMAGE} AS uploader ARG IMMICH_GO_VERSION=0.32.0 ARG IMMICH_GO_SHA256_AMD64=6e2ad86bafdadb9466d6515de7cb882726c0aea1a21d51164dff361d7d480a97 ARG IMMICH_GO_SHA256_ARM64=2c35d9284baae407ef9540bdac5f488971b0bdc7be758a4d7c05ab270af09fdb COPY docker/fetch-immich-go.py /tmp/fetch-immich-go.py RUN python /tmp/fetch-immich-go.py \ --version "${IMMICH_GO_VERSION}" \ --sha256-amd64 "${IMMICH_GO_SHA256_AMD64}" \ --sha256-arm64 "${IMMICH_GO_SHA256_ARM64}" \ --into /usr/local/bin \ && /usr/local/bin/immich-go version # ── the application ────────────────────────────────────────────────────────── FROM ${PYTHON_IMAGE} AS runtime ARG EXIFTOOL_VERSION=13.25+dfsg-1 ARG IMMICH_GO_VERSION=0.32.0 # The library is mounted from the host, so the container's identity must match the # ownership that library already has: everything this application renames, writes # EXIF into, or archives has to stay owned by the host user afterwards. ARG UID=1000 ARG GID=1000 LABEL org.opencontainers.image.title="photo_pipeline" \ org.opencontainers.image.source="https://github.com/domverse/photoanalyzer" \ io.photoanalyzer.exiftool.version="${EXIFTOOL_VERSION}" \ io.photoanalyzer.immich-go.version="${IMMICH_GO_VERSION}" ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PATH=/opt/venv/bin:$PATH \ PHOTO_PIPELINE_DATA_DIR=/data RUN set -eu; \ apt-get update; \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ "libimage-exiftool-perl=${EXIFTOOL_VERSION}"; \ rm -rf /var/lib/apt/lists/* COPY --from=uploader /usr/local/bin/immich-go /usr/local/bin/immich-go WORKDIR /app COPY pyproject.toml alembic.ini README.md ./ COPY photo_pipeline ./photo_pipeline COPY migrations ./migrations COPY frontend ./frontend COPY docker/entrypoint.sh docker/healthcheck.sh /usr/local/bin/ # Runtime dependencies only: the `test` extra (pytest, playwright) and the `vision` # extra stay out, and pip's build isolation leaves no build tooling behind. RUN set -eu; \ python -m venv /opt/venv; \ /opt/venv/bin/pip install --no-cache-dir -e . # What is installed must be what was pinned, or the labels and the version record # would be a claim rather than a fact. RUN set -eu; \ mkdir -p /etc/photo-pipeline; \ exiftool_version="$(exiftool -ver)"; \ immich_go_version="$(immich-go version | head -n 1 | tr -d '\r')"; \ expected_exiftool="$(printf '%s' "${EXIFTOOL_VERSION}" | cut -d+ -f1 | cut -d- -f1)"; \ [ "${exiftool_version}" = "${expected_exiftool}" ] \ || { echo "exiftool ${exiftool_version} is not the pinned ${expected_exiftool}" >&2; exit 1; }; \ case "${immich_go_version}" in \ *"${IMMICH_GO_VERSION}"*) ;; \ *) echo "immich-go '${immich_go_version}' is not pinned ${IMMICH_GO_VERSION}" >&2; exit 1 ;; \ esac; \ printf '{\n "exiftool": "%s",\n "immich-go": "%s"\n}\n' \ "${exiftool_version}" "${IMMICH_GO_VERSION}" > /etc/photo-pipeline/versions.json # Non-root, with the host library's ownership. /data is the persistent volume; the # photo library itself is mounted by the deployment (US08-03), never baked in. RUN set -eu; \ groupadd --gid "${GID}" --non-unique app; \ useradd --uid "${UID}" --gid "${GID}" --non-unique --no-create-home --home-dir /app app; \ mkdir -p /data; \ chown "${UID}:${GID}" /data USER ${UID}:${GID} EXPOSE 8000 # Readiness, not liveness: an unmigrated or misconfigured database answers # /api/v1/health/ready with 503, and a container that cannot serve must not be # reported healthy. The worker role has no endpoint, so its check is a no-op here. HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD ["/usr/local/bin/healthcheck.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["serve"]