How to cut your Python Docker build times in half with uv

March 4, 2024 update: This blog has been updated to reflect additions to uv as of 0.1.12, specifically the --system flag. See Addendum section for more information.


When I heard that Charlie Marsh, the creator of Ruff, created a fast replacement for pip called uv, I dropped everything I was doing and added it to all my repos. It’s great. It cut my build times in half. You should be using it in all your projects.

Here’s a TLDR of how to use it in Docker, assuming you are using the Python base image.

Imagine you have the following Dockerfile:

FROM python:3.12

COPY requirements.txt /requirements.txt

RUN pip install --no-cache-dir -r requirements.txt

COPY hello.py /hello.py

CMD ["python", "hello.py"]

Here is what it looks like with uv:

FROM python:3.12

ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh

COPY requirements.txt /requirements.txt

RUN /root/.cargo/bin/uv pip install --system --no-cache -r requirements.txt

COPY hello.py /hello.py

CMD ["python", "hello.py"]

And there you go.

Explanation of the choices in the code:

  • ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh and then RUN /install.sh && rm /install.sh installs uv at the location /root/.cargo/bin/uv.
    • You can also pip install --no-cache-dir "uv~=0.1" instead of curl or ADD if you want, but in testing the curl command is about 2-3 seconds faster.
    • Why not “RUN curl -LsSf https://astral.sh/uv/install.sh | sh“? Because this does not install updates when uv is updated. In a worst case scenario when uv releases a 1.0 with a breaking change, this can cause your local builds to pass (since it’s using a cached old version) while your remote builds fail. ADD is better assurance that your local and remote builds will stay in sync, also you get to keep your version of uv updated and that’s most likely not a bad thing. If you’d like to keep using the cache, then you can do RUN curl instead of ADD.
  • You want to install uv near the top to take advantage of layer caching. Similarly, for layering reasons, you want to pip install before COPYing the Python code.

Have fun! And be sure to than Charlie Marsh and major contributors Jacob Finkelman and Matthieu Pizenberg for their incredible work in making the Python ecosystem so much better!

Addendum (March 4, 2024)

A previous version of this blog post, corresponding with uv versions 0.1.0 through 0.1.11, suggested using the following:

FROM python:3.12

ENV VIRTUAL_ENV=/usr/local
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh

COPY requirements.txt /requirements.txt

RUN /root/.cargo/bin/uv pip install --no-cache -r requirements.txt

COPY hello.py /hello.py

CMD ["python", "hello.py"]

With the following blurb about why the VIRTUAL_ENV environment variable was included:

What’s up with the VIRTUAL_ENV environment variable? Long story short, the Python executable for the Python base image is located at /usr/local/bin/python. uv is insistent on users utilizing venvs, but you can bypass this by just defining your “venv” to be where the global Python installation is inside the Docker image. uv recognizes the VIRTUAL_ENV env var internally.

With uv version 0.1.12, this approach is no longer necessary due to the --system flag.

This blog post is mostly a relic of the literal first few hours after uv came out. This post was originally written to provide an optimal Docker installation for uv 0.1.0, since it was not clear in 0.1.0 how to best install and use uv in a Dockerfile, and I was so excited for the project that I wanted to make sure everyone had access to it for their builds. The maintainers work incredibly quickly, and have made this a lot easier in 0.12.0, so this blog post is a little less necessary. Still, I feel compelled to keep this up to date, in case anyone else stumbles upon this post.