uv icon indicating copy to clipboard operation
uv copied to clipboard

Cache invalidation on local dependencies when using docker

Open hxyannay opened this issue 1 year ago • 18 comments

Hello.

I am having difficulties using uv in docker. My project structure has two python libraries, one is an API server, and the other is an internal library. The problem I am encountering is that after building, when running the image, the uv run reinstalls my internal libraries, with the following message: Requirement installed, but not fresh: .... This is problematic, because as per docker best practices, it is good to ensure that the install actually happens on the build stage, and not in the actual running phase of my application. When debugging the issue further, I found out that it happens because the ctime of my pyproject.toml of the internal library changes between the docker build and the actual run.

I finally validated that symptom using the following dockerfile:

FROM ubuntu:22.04

RUN touch a && stat a > /root/stat-a
RUN sleep 5

And running: docker build --no-cache . -t playground && docker run playground sh -c 'cat /root/stat-a && stat a' The output that I received was:

  File: a
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: e3h/227d        Inode: 1336246     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2024-11-18 10:04:52.462498005 +0000
Modify: 2024-11-18 10:04:52.462498005 +0000
Change: 2024-11-18 10:04:52.462498005 +0000
 Birth: 2024-11-18 10:04:52.462498005 +0000
---
  File: a
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: e3h/227d        Inode: 225316      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2024-11-18 10:04:52.000000000 +0000
Modify: 2024-11-18 10:04:52.000000000 +0000
Change: 2024-11-18 10:04:57.680498008 +0000
 Birth: 2024-11-18 10:04:57.680498008 +0000

As seen, two things are changing between the build and run - both the inode number, and the Change & Birth times. This seems like an odd behavior of docker, but as I dived deeper it seems inherent to the layering system of docker.

I think that the solution to that issue should come from uv's side, as the usage for similar type of projects in docker should be very common.

A possible approach that would solve both the cache freshness problem and would work with docker's odd behavior would be using checksums for local dependencies freshness check.

Some additional information:

  • uv version: v0.5.1
  • Platform: MacOS with Docker Desktop

Thanks in advance! Yannay

hxyannay avatar Nov 18 '24 10:11 hxyannay

Thanks for the write-up. I think it'd be much easier to help out if you could provide a full reproduction for the behavior you're seeing, just so I can understand how you're structuring and running your application.

charliermarsh avatar Nov 18 '24 13:11 charliermarsh

Hey, So I worked a bit to reproduce it, created this fork of the example from the official documentation of uv Added one commit that changes the command to use uv run instead of modifying the ENV (which is better anyways in my opinion).

When running the build and then run:

$ docker build -t uv-example .
$ docker run uv-example
Uninstalled 1 package in 0.68ms
Installed 2 packages in 10ms
...

As seen in the last two rows, it uninstalled and reinstalled the package of the example, as the symptoms that I encountered.

Thanks for responding so fast!

hxyannay avatar Nov 18 '24 18:11 hxyannay

Thanks! Interestingly, with that example I see:

❯ docker run uv-example
Installed 1 package in 6ms
Bytecode compiled 1143 files in 49ms
INFO     Using path src/uv_docker_example
INFO     Resolved absolute path /app/src/uv_docker_example

Am I misunderstanding the output, or does it look like a platform difference?

charliermarsh avatar Nov 18 '24 20:11 charliermarsh

Yeah it does seem different, I am running this on MacOS with docker desktop. Though in your output it still shows that it installed a package, is it the expected behavior? I would expect it to not install any package as we run uv sync in the dockerfile.

hxyannay avatar Nov 18 '24 20:11 hxyannay

Sorry, I haven't looked at all -- I just noticed that it differed from the above so wanted to ask first. (I'm also on macOS with Docker Desktop.)

charliermarsh avatar Nov 18 '24 20:11 charliermarsh

This is very odd then. I am running on M2 with macOS Sequoia 15.1 and Docker Desktop v4.35.1 with docker engine v27.3.1

hxyannay avatar Nov 18 '24 20:11 hxyannay

Are ya'll definitely testing the same uv version? The image could be cached if you're just using that example repository.

zanieb avatar Nov 18 '24 20:11 zanieb

So in my case at least, the installed package in Installed 1 package is Ruff, due to the use of --no-dev when syncing the Dockerfile. (uv run will sync dev by default.)

charliermarsh avatar Nov 19 '24 03:11 charliermarsh

Have you considered using uv run --frozen in your CMD instead? I think that's more appropriate given that you may not even want to re-validate the lockfile on container startup.

charliermarsh avatar Nov 19 '24 03:11 charliermarsh

(I'm also on Docker version 27.3.1. M3, Sonoma 14.5.)

charliermarsh avatar Nov 19 '24 03:11 charliermarsh

Still the same when running with --frozen flag. The uv version from inside the container is 0.5.3. Could it be related to the VM options in docker? I am using Apple Virtualization framework with Rosetta enabled and VirtioFS for file sharing implementation.

hxyannay avatar Nov 20 '24 08:11 hxyannay

(I'm also on Docker version 27.3.1. M3, Sonoma 14.5.)

Hey @charliermarsh, is there any more information I can share that will help you reproduce it locally?

hxyannay avatar Nov 24 '24 12:11 hxyannay

I also cannot reproduce (similarly, on an M3 with Docker Desktop)

❯ docker build -t uv-example .
[+] Building 3.0s (10/10) FINISHED                                                                                                                                                                                                                        docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                                                                                                                                      0.0s
 => => transferring dockerfile: 1.18kB                                                                                                                                                                                                                                    0.0s
 => [internal] load metadata for ghcr.io/astral-sh/uv:0.5.3-python3.12-bookworm-slim                                                                                                                                                                                      0.8s
 => [internal] load .dockerignore                                                                                                                                                                                                                                         0.0s
 => => transferring context: 106B                                                                                                                                                                                                                                         0.0s
 => [stage-0 1/5] FROM ghcr.io/astral-sh/uv:0.5.3-python3.12-bookworm-slim@sha256:090b8acad6ffaeb1fbbec6ed7b052c3837ae6c407b5164d0b09e61e9a38311ab                                                                                                                        1.2s
 => => resolve ghcr.io/astral-sh/uv:0.5.3-python3.12-bookworm-slim@sha256:090b8acad6ffaeb1fbbec6ed7b052c3837ae6c407b5164d0b09e61e9a38311ab                                                                                                                                0.0s
 => => sha256:6c1729b16fda6af1e58192a3723fb0a8229a126ea9ee1e73ff5a2aae275ca1fb 1.25kB / 1.25kB                                                                                                                                                                            0.0s
 => => sha256:b0751293b2c29a56170f36ed84d79f872bb2923e426099166290e46865865459 6.35kB / 6.35kB                                                                                                                                                                            0.0s
 => => sha256:2d87e44c35ccf73088373150fe7de8d6b2b2958dd33d8d0c2825048a687a4f7e 13.52MB / 13.52MB                                                                                                                                                                          1.1s
 => => sha256:090b8acad6ffaeb1fbbec6ed7b052c3837ae6c407b5164d0b09e61e9a38311ab 2.22kB / 2.22kB                                                                                                                                                                            0.0s
 => => extracting sha256:2d87e44c35ccf73088373150fe7de8d6b2b2958dd33d8d0c2825048a687a4f7e                                                                                                                                                                                 0.1s
 => [internal] load build context                                                                                                                                                                                                                                         0.0s
 => => transferring context: 205.50kB                                                                                                                                                                                                                                     0.0s
 => [stage-0 2/5] WORKDIR /app                                                                                                                                                                                                                                            0.0s
 => [stage-0 3/5] RUN --mount=type=cache,target=/root/.cache/uv     --mount=type=bind,source=uv.lock,target=uv.lock     --mount=type=bind,source=pyproject.toml,target=pyproject.toml     uv sync --frozen --no-install-project --no-dev                                  0.4s
 => [stage-0 4/5] ADD . /app                                                                                                                                                                                                                                              0.0s
 => [stage-0 5/5] RUN --mount=type=cache,target=/root/.cache/uv     uv sync --frozen --no-dev                                                                                                                                                                             0.4s
 => exporting to image                                                                                                                                                                                                                                                    0.1s 
 => => exporting layers                                                                                                                                                                                                                                                   0.1s 
 => => writing image sha256:be88ded41d630245cb1ef290561c9885f61b039ef7381cced4de0abf15de5bdf                                                                                                                                                                              0.0s
 => => naming to docker.io/library/uv-example                                                                                                                                                                                                                             0.0s

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview 
❯ docker run uv-example
Installed 1 package in 14ms
diff --git a/Dockerfile b/Dockerfile
index d7acf90..f0aed3f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Use a Python image with uv pre-installed
-FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
+FROM ghcr.io/astral-sh/uv:0.5.3-python3.12-bookworm-slim
 
 # Install the project into `/app`
 WORKDIR /app

zanieb avatar Nov 25 '24 23:11 zanieb

I'm also using the default settings, which afaict includes VirtioFS, Rosetta, and the Virtualization framework.

zanieb avatar Nov 25 '24 23:11 zanieb

Same issue on my side - following this example leads to Uninstalled 1 package [..] Installed 1 package. Using --verbose on uv run surfaces "Requirement installed, but not fresh", with the requirement being the project itself.

My colleagues using Docker Desktop on Windows are encountering this as well. Is there a way to bypass or "trick" the freshness check? How does uv decide whether a package is not fresh and requires rebuilding?

czechnology avatar May 12 '25 12:05 czechnology

Me and other my team mates are encountering this issue. Everyone with the issue is running Docker Desktop, either on macOS or Windows. Team mates who have installed Docker community edition in WSL2 do not have this issue. We noticed the issue rather quickly after we switched to using uv because we:

  1. run docker build where Dockerfile runs uv to install dependencies using uv sync --frozen
  2. using the image from step 1, we docker run --rm --network none uv run --frozen pytest (and pylint and other tools)

The step 2 fails for people using Docker Desktop because uv tries to rebuild the project and download some libraries (setuptools). Step 2 succeeds for people using Docker community edition.

Of course we can work around the problem by removing --network none but it revealed this problem that uv tries to rebuild the python project during docker run.

stisti avatar May 12 '25 13:05 stisti

Is there any workaround? I don't want to re-install anything on container startup.


Use this command

.venv/bin/python main.py

instead of

uv run main.py

Nugine avatar May 17 '25 15:05 Nugine

We are switching to Chainguard images, which requires a multi-stage build where you build in one stage with the "-dev" container, and copy the build artifacts into the final stage which doesn't have /bin/sh, etc.

The Docker COPY command doesn't preserve timestamps, so in the final stage when it tries to run the program:

CMD [ "uv", "run", ... ]

uv says the "Requirement installed, but not fresh" for the project itself and it tries to reinstall.

We can't just call the project directly vs uv run because the library we use (Prefect) calls uv run internally with lots of arguments l don't have control over like --python and --with

I would like an environment variable that tells uv not to try and do anything and just run the program.

edit it seems this combination of environment variables at the end of the Dockerfile works:

ENV UV_NO_MANAGED_PYTHON=true \
    UV_SYSTEM_PYTHON=true \
    UV_LOCKED=1 \
    UV_NO_SYNC=1 \
    UV_NO_CACHE=1 \
    UV_NO_DEV=1

jmesterh avatar Oct 18 '25 18:10 jmesterh