2026, Jan 11 17:00

Stop pip installs disappearing in Python multi-stage Docker: install in the builder and reuse the venv

Why pip is present but dependencies are missing in Python multi-stage Docker, and how to fix by installing in builder and copying the venv into the final image

When a Python multi-stage Docker build seemingly “has pip” but none of your dependencies land in site-packages, you lose time chasing ghosts. A common manifestation: the image builds cleanly, you can exec into the container, pip is there, yet imports like pydantic fail. Below is a concise walkthrough of what causes this pattern and how to fix it by installing your project and its dependencies in the builder stage and using the same virtual environment in the final image.

Reproducing the issue

Consider a two-stage Dockerfile where a virtual environment is created in the first stage and copied into the final image. The last step tries to install a package but nothing ends up in the venv’s site-packages.

# build stage
FROM python:3.13-slim AS pack-img

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

RUN pip install --no-cache-dir --upgrade pip

# final stage
FROM python:3.13-slim

COPY --from=pack-img /opt/venv /opt/venv

WORKDIR /srv
ENV HOME=/srv
ENV PATH="/opt/venv/bin:$PATH"

RUN addgroup --system svc && adduser --system --group svc

COPY . .

RUN chown -R svc:svc $HOME && chown -R svc:svc "/opt/venv/"

USER svc

RUN pip install -e pydantic

The image builds without an error, yet you open /opt/venv/lib/.../site-packages and find only pip. Running the container with a neutral command such as tail -f /dev/null confirms the package still isn’t there.

What actually goes wrong

The multi-stage setup prepares a virtual environment but doesn’t populate it with your project’s dependencies during the stage that’s meant for building. As a result, the venv copied to the final stage has nothing installed except pip. Installing packages in the last stage under a non-root user doesn’t address the core problem and keeps the final image detached from the build step where your project should be resolved.

There’s another subtlety you must get right in the final image: the PATH must include the venv’s bin directory so you are invoking the correct pip and python. If PATH doesn’t point to /opt/venv/bin in the final stage, you won’t be using the venv you copied across, which can make the behavior look inconsistent.

The fix: install your project in the builder, copy the venv forward

Install your project (and thus its dependencies defined in pyproject.toml) in the builder stage, then carry the fully populated /opt/venv into the final image. Only then switch users. This approach makes the dependency state deterministic and avoids the final-stage install trap.

FROM python:3.13-slim AS pack-img

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

RUN pip install --no-cache-dir --upgrade pip setuptools wheel

WORKDIR /srv

COPY pyproject.toml ./
COPY vector_db_service ./vector_db_service

RUN pip install --no-cache-dir .

FROM python:3.13-slim

COPY --from=pack-img /opt/venv /opt/venv

WORKDIR /srv
ENV HOME=/srv
ENV PATH="/opt/venv/bin:$PATH"

RUN addgroup --system svc && adduser --system --group svc

COPY . .

RUN chown -R svc:svc $HOME && chown -R svc:svc "/opt/venv/"

USER svc

This sequence ensures the builder has the files necessary to resolve the project and install its dependencies. The final image inherits the exact virtual environment with everything already installed. Notice that PATH points to /opt/venv/bin in both stages, so pip and python refer to the same venv throughout.

Why this matters

Handling installs in the builder stage makes builds reproducible and images predictable. You can open /opt/venv/lib/.../site-packages and find your dependencies reliably. It also removes ambiguity about which pip is being used and prevents silent mismatches between system and virtualenv tools. Finally, this pattern scales cleanly: you copy only what’s needed to resolve your project, install once, and reuse the result.

Takeaways

Set up the virtual environment up front, install the project in the builder stage using the files that declare its dependencies, and copy the venv into the final image. Keep the PATH pointing at /opt/venv/bin in both stages so you’re consistently using the same environment. If you need to inspect what’s installed, start the container with a neutral command and verify the contents of site-packages. Following this flow removes the guesswork and makes Python dependency installation in multi-stage Docker builds straightforward.