116 lines
5.3 KiB
Docker
116 lines
5.3 KiB
Docker
# 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"]
|