diff --git a/.github/workflows/build-and-publish-docker-image.yml b/.github/workflows/build-and-publish-docker-image.yml index 3d82d8c4..3a8672c1 100644 --- a/.github/workflows/build-and-publish-docker-image.yml +++ b/.github/workflows/build-and-publish-docker-image.yml @@ -76,6 +76,11 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} + # Runners are ephemeral, so without this every layer is rebuilt on every + # run: the pinned Chromium install, the dependency syncs, the driver + # setup. Scoped per target so the two images do not evict each other. + cache-from: type=gha,scope=${{ matrix.target }} + cache-to: type=gha,scope=${{ matrix.target }} - name: Generate artifact attestation for ${{ matrix.target }} image if: github.event_name != 'pull_request' diff --git a/Dockerfile b/Dockerfile index c606e95e..799e5b1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,12 +33,6 @@ FROM ghcr.io/astral-sh/uv:0.12.16@sha256:adc68cd785ca65ea25c0611043b0a00b4ea3a22 # Use python-slim as the base image FROM python:3.14.7-slim@sha256:cad9a2c871761c413caa6fdd6441c783451e740a48aaeba60ae62a8b53525ef6 AS base -# Add build argument for version -ARG BUILD_VERSION -ENV BUILD_VERSION=${BUILD_VERSION} -ARG RELEASE_VERSION -ENV RELEASE_VERSION=${RELEASE_VERSION} - # Set shell to bash with pipefail option SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -127,26 +121,13 @@ RUN rm -rf \ /usr/local/lib/python*/site-packages/pip \ /usr/local/lib/python*/site-packages/pip-*.dist-info -# Copy application code *after* dependencies are installed -COPY . . - -# Copy built frontend from frontend-builder stage -COPY --from=frontend-builder /frontend/dist /app/frontend-dist - -# Final setup: create image-owned runtime paths for the fixed non-root user. -# Root/PUID mode still re-homes ownership at startup when needed. -RUN mkdir -p \ - /config \ - /books \ - /var/log/shelfmark \ - /tmp/shelfmark/seleniumbase/downloaded_files \ - /tmp/shelfmark/seleniumbase/archived_files && \ - rm -rf /app/downloaded_files /app/archived_files && \ - ln -s /tmp/shelfmark/seleniumbase/downloaded_files /app/downloaded_files && \ - ln -s /tmp/shelfmark/seleniumbase/archived_files /app/archived_files && \ - chown -R 1000:1000 /config /books /home/shelfmark /tmp/shelfmark /var/log/shelfmark && \ - chmod -R a+rX /app && \ - chmod +x /app/entrypoint.sh /app/tor.sh /app/wireguard.sh /app/genDebug.sh +# The application code is deliberately NOT copied here. `base` is shared by the +# final stages, so a COPY of the source at this point invalidates every layer +# built on top of it -- the Chromium install, the browser dependency sync and +# the SeleniumBase driver download -- on any source change. Each stage copies +# the source as its last step instead, so a code-only rebuild rewrites one small +# layer and every expensive layer is reused. `[tool.uv] package = false` is what +# makes this safe: no `uv sync` needs the project source. # Expose the application port EXPOSE ${FLASK_PORT} @@ -237,11 +218,72 @@ RUN SELENIUMBASE_DRIVERS_DIR=$(/app/.venv/bin/python -c "import pathlib, seleniu # Grant read/execute permissions to others RUN chmod -R o+rx /usr/bin/chromium +# --- Application code: last, so every expensive layer above stays cached --- + +COPY . . + +COPY --from=frontend-builder /frontend/dist /app/frontend-dist + +# Image-owned runtime paths for the fixed non-root user. Root/PUID mode still +# re-homes ownership at startup when needed. +RUN mkdir -p \ + /config \ + /books \ + /var/log/shelfmark \ + /tmp/shelfmark/seleniumbase/downloaded_files \ + /tmp/shelfmark/seleniumbase/archived_files && \ + rm -rf /app/downloaded_files /app/archived_files && \ + ln -s /tmp/shelfmark/seleniumbase/downloaded_files /app/downloaded_files && \ + ln -s /tmp/shelfmark/seleniumbase/archived_files /app/archived_files && \ + chown -R 1000:1000 /config /books /home/shelfmark /tmp/shelfmark /var/log/shelfmark && \ + chmod -R a+rX /app && \ + chmod +x /app/entrypoint.sh /app/tor.sh /app/wireguard.sh /app/genDebug.sh + # Default command to run the application entrypoint script + +# Version stamp last. These carry the commit sha, so they change on every +# build and everything below them rebuilds. Kept here, the dependency and +# browser layers above stay valid and a pull only fetches what changed. +ARG BUILD_VERSION +ENV BUILD_VERSION=${BUILD_VERSION} +ARG RELEASE_VERSION +ENV RELEASE_VERSION=${RELEASE_VERSION} + CMD ["/app/entrypoint.sh"] + FROM base AS shelfmark-lite ENV USING_EXTERNAL_BYPASSER=true +# --- Application code: last, so every expensive layer above stays cached --- + +COPY . . + +COPY --from=frontend-builder /frontend/dist /app/frontend-dist + +# Image-owned runtime paths for the fixed non-root user. Root/PUID mode still +# re-homes ownership at startup when needed. +RUN mkdir -p \ + /config \ + /books \ + /var/log/shelfmark \ + /tmp/shelfmark/seleniumbase/downloaded_files \ + /tmp/shelfmark/seleniumbase/archived_files && \ + rm -rf /app/downloaded_files /app/archived_files && \ + ln -s /tmp/shelfmark/seleniumbase/downloaded_files /app/downloaded_files && \ + ln -s /tmp/shelfmark/seleniumbase/archived_files /app/archived_files && \ + chown -R 1000:1000 /config /books /home/shelfmark /tmp/shelfmark /var/log/shelfmark && \ + chmod -R a+rX /app && \ + chmod +x /app/entrypoint.sh /app/tor.sh /app/wireguard.sh /app/genDebug.sh + + +# Version stamp last. These carry the commit sha, so they change on every +# build and everything below them rebuilds. Kept here, the dependency and +# browser layers above stay valid and a pull only fetches what changed. +ARG BUILD_VERSION +ENV BUILD_VERSION=${BUILD_VERSION} +ARG RELEASE_VERSION +ENV RELEASE_VERSION=${RELEASE_VERSION} + CMD ["/app/entrypoint.sh"]