The image has not built since #157: its uv sync runs before the source is copied #166

Closed
opened 2026-09-10 11:15:44 +00:00 by tiagoagueda · 1 comment
Owner

Observation

Deploying the 0.3.0 branch to the test instance (for #163) failed in the image build, in the build stage:

#18 [build 5/9] RUN uv sync --locked --no-default-groups --extra server --no-cache
#18 2.544    Building postulo @ file:///app
#18 2.600   × Failed to build `postulo @ file:///app`
#18 2.602   ╰─▶ Expected a Python module at: src/postulo/__init__.py

A failed build leaves the running container alone, so the instance kept running the image from 8 September and nothing was lost.

Cause

4346dfa5 (#157) gave the Python build its own stage and, in the same move, put dependencies first so that a change to the application would not re-resolve them:

COPY pyproject.toml uv.lock README.md ./
RUN uv sync --locked --no-default-groups --extra server --no-cache
COPY src ./src

uv sync installs the project as well as its dependencies unless told otherwise, and at that point there is no src/. Nothing syncs again after the source arrives. Before #157 the file copied src first, which is why the test instance's image from 8 September built.

Why nobody saw it

The commit says it plainly: "Read rather than measured, because nothing here builds an image (#81)." CI never builds the image, and tests/test_image_build.py reads the Dockerfile rather than running it. This deploy was the first build since, so no image of the 0.3.0 branch can have been built since 4346dfa5, and the release workflow's image job would fail the same way.

What fixing it is

uv's documented pattern for exactly this (Docker guide, intermediate layers): sync the dependencies with --no-install-project before the source is copied, and sync again after it to install the project.

COPY pyproject.toml uv.lock README.md ./
RUN uv sync --locked --no-default-groups --extra server --no-cache --no-install-project
COPY src ./src
RUN uv sync --locked --no-default-groups --extra server --no-cache

The layer caching #157 wanted survives: a change to src re-runs only the second sync, which installs one project and no dependencies.

And a test that reads the order. tests/test_image_build.py already splits the file into stages (#157). It can assert that a sync before COPY src passes --no-install-project, and that one after it installs the project. It currently insists on exactly one uv sync line, so that part changes too. That is not a substitute for #81, which is the real fix for "nothing here builds an image".

Worth being careful about

  • Build it before saying so. The test instance is the only place an image is built today. The fix should be confirmed there, not read.
  • --no-install-project belongs only on the first sync. On both lines, the image would ship without Postulo installed in its environment, which is the same failure at run time instead of at build time.

Classification

Bug. Blocks building the 0.3.0 image, and so the release.

## Observation Deploying the `0.3.0` branch to the test instance (for #163) failed in the image build, in the `build` stage: ``` #18 [build 5/9] RUN uv sync --locked --no-default-groups --extra server --no-cache #18 2.544 Building postulo @ file:///app #18 2.600 × Failed to build `postulo @ file:///app` #18 2.602 ╰─▶ Expected a Python module at: src/postulo/__init__.py ``` A failed build leaves the running container alone, so the instance kept running the image from 8 September and nothing was lost. ## Cause `4346dfa5` (#157) gave the Python build its own stage and, in the same move, put **dependencies first** so that a change to the application would not re-resolve them: ```dockerfile COPY pyproject.toml uv.lock README.md ./ RUN uv sync --locked --no-default-groups --extra server --no-cache COPY src ./src ``` `uv sync` installs the project as well as its dependencies unless told otherwise, and at that point there is no `src/`. Nothing syncs again after the source arrives. Before #157 the file copied `src` first, which is why the test instance's image from 8 September built. ## Why nobody saw it The commit says it plainly: *"Read rather than measured, because nothing here builds an image (#81)."* CI never builds the image, and `tests/test_image_build.py` reads the Dockerfile rather than running it. This deploy was the first build since, so **no image of the 0.3.0 branch can have been built since `4346dfa5`**, and the release workflow's image job would fail the same way. ## What fixing it is uv's documented pattern for exactly this ([Docker guide, *intermediate layers*](https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers)): sync the dependencies with `--no-install-project` before the source is copied, and sync again after it to install the project. ```dockerfile COPY pyproject.toml uv.lock README.md ./ RUN uv sync --locked --no-default-groups --extra server --no-cache --no-install-project COPY src ./src RUN uv sync --locked --no-default-groups --extra server --no-cache ``` The layer caching #157 wanted survives: a change to `src` re-runs only the second sync, which installs one project and no dependencies. **And a test that reads the order.** `tests/test_image_build.py` already splits the file into stages (#157). It can assert that a sync before `COPY src` passes `--no-install-project`, and that one after it installs the project. It currently insists on exactly one `uv sync` line, so that part changes too. That is not a substitute for #81, which is the real fix for "nothing here builds an image". ## Worth being careful about - **Build it before saying so.** The test instance is the only place an image is built today. The fix should be confirmed there, not read. - **`--no-install-project` belongs only on the first sync.** On both lines, the image would ship without Postulo installed in its environment, which is the same failure at run time instead of at build time. ## Classification Bug. Blocks building the 0.3.0 image, and so the release.
tiagoagueda added this to the 0.3.0 milestone 2026-09-10 11:15:44 +00:00
Author
Owner

Done in b6cfb8f7, and built rather than read: the test instance's deploy built the image and is running it.

The fix

As proposed, uv's documented pattern:

RUN uv sync --locked --no-default-groups --extra server --no-cache --no-install-project
COPY src ./src
COPY scripts/messages.py ./scripts/messages.py
RUN uv sync --locked --no-default-groups --extra server --no-cache

Checked on the instance

  • Image postulo:latest Built; the container was recreated, started and reports healthy, and /healthz answers ok.
  • WeasyPrint 70.0 imports in it even with deprecation warnings as errors, so HarfBuzz-Subset (#163) is there, and the renderer it picks is WeasyPrint.
  • Every migration applied. The only system-check warning is the deliberate SECURE_SSL_REDIRECT one (#82).
  • The image is 457 MB.

Tests

test_the_project_is_installed_once_its_source_is_there reads the build stage: a sync before COPY src must pass --no-install-project, and one after it must install the project. On the old Dockerfile it fails with runs before there is a project. The other sync tests now check every sync line rather than insisting on exactly one.

Worth knowing

This is the third mistake in the Dockerfile that reading it did not catch, after #121 and #154. #81 is still the real fix: a CI job that builds the image.

Done in `b6cfb8f7`, and **built rather than read**: the test instance's deploy built the image and is running it. ## The fix As proposed, uv's documented pattern: ```dockerfile RUN uv sync --locked --no-default-groups --extra server --no-cache --no-install-project COPY src ./src COPY scripts/messages.py ./scripts/messages.py RUN uv sync --locked --no-default-groups --extra server --no-cache ``` ## Checked on the instance - `Image postulo:latest Built`; the container was recreated, started and reports **healthy**, and `/healthz` answers `ok`. - WeasyPrint 70.0 imports in it **even with deprecation warnings as errors**, so HarfBuzz-Subset (#163) is there, and the renderer it picks is WeasyPrint. - Every migration applied. The only system-check warning is the deliberate `SECURE_SSL_REDIRECT` one (#82). - The image is 457 MB. ## Tests `test_the_project_is_installed_once_its_source_is_there` reads the `build` stage: a sync before `COPY src` must pass `--no-install-project`, and one after it must install the project. On the old Dockerfile it fails with *runs before there is a project*. The other sync tests now check every sync line rather than insisting on exactly one. ## Worth knowing This is the third mistake in the Dockerfile that reading it did not catch, after #121 and #154. #81 is still the real fix: a CI job that builds the image.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#166
No description provided.