DEV Community

Cover image for Faster Pip Installs in Python Docker Images With BuildKit Cache Mounts
Aniket Abhishek Soni
Aniket Abhishek Soni

Posted on AI-assisted

Faster Pip Installs in Python Docker Images With BuildKit Cache Mounts

Why this matters: In data engineering, Python environments often require heavy numerical and machine learning libraries that take minutes to download and install. BuildKit cache mounts persist these downloaded packages across builds, transforming sluggish container image creation into a cache-efficient process that significantly reduces CI/CD wait times.

Consider a hypothetical pipeline that processes sensor data using a heavy stack of libraries: pandas, scikit-learn, numpy, and pyarrow. Every time a developer pushes a code change to the repository, the CI/CD runner triggers a fresh Docker build. Because the pip install layer clears its internal cache after every run, the runner re-downloads the same multi-megabyte wheels from PyPI on every single commit.

Symptoms

Engineers often notice this performance bottleneck during the "Build" phase of a CI pipeline. The following indicators are common:

  1. High CI/CD duration: The build log shows the pip install step hanging for several minutes, despite the requirements.txt file having only minor changes.
  2. Network egress costs: In environments with strict egress monitoring, you might see an unnecessary spike in traffic to files.pythonhosted.org on every build.
  3. False leads: Engineers often try to fix this by "layer optimization," such as grouping pip install commands or using --no-cache-dir. While --no-cache-dir keeps images small, it forces the network overhead every time, which is the root of the latency.

blue and white light illustration
Photo by Compare Fibre on Unsplash

1. The missing cache mount

The most common cause of slow builds is failing to instruct the Docker builder to persist the pip cache directory across stages. By default, the pip cache exists only during the execution of a single RUN command.

How to confirm: Check your Dockerfile. If your pip install command does not include the --mount=type=cache flag, your builder is discarding the cache after every step.

The Fix: Update your RUN instruction to utilize BuildKit's cache mounting.

# syntax=docker/dockerfile:1
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .

# Mount the pip cache directory
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Note: The # syntax line is required to enable BuildKit features.


2. Incorrect cache target mapping

Even when using cache mounts, if the pip cache directory is not explicitly mapped to the environment variable PIP_CACHE_DIR, pip might still attempt to write to its default location, missing the mount point.

How to confirm: Inspect the build output. If you see "Downloading" for every package despite having the mount flag, the cache is not being utilized.

The Fix: Explicitly set the PIP_CACHE_DIR environment variable to match the path defined in your mount.

# syntax=docker/dockerfile:1
FROM python:3.11-slim

ENV PIP_CACHE_DIR=/root/.cache/pip

WORKDIR /app
COPY requirements.txt .

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --upgrade pip && \
    pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

3. Inefficient layer ordering

If you COPY your application source code before running pip install, you invalidate the cache every time a single line of code changes. This forces Docker to rebuild the entire pip layer, rendering the cache mount useless for speed.

How to confirm: Watch the build log. If you see "COPY ." triggering a rebuild of the RUN pip install layer, your ordering is wrong.

The Fix: Copy only the requirements file first, run the install, and then copy the rest of your application code.

# syntax=docker/dockerfile:1
FROM python:3.11-slim

WORKDIR /app

# Copy requirements first to leverage cache
COPY requirements.txt .

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Copy the rest of the application
COPY . .
Enter fullscreen mode Exit fullscreen mode

a large ship in the water
Photo by Nathan Cima on Unsplash

4. Permission conflicts with non-root users

To follow security best practices, we often use USER instructions to drop privileges. However, if the pip cache mount is owned by root, a non-root user will lack write permissions, causing the install to fail or skip the cache.

How to confirm: The build fails with Permission denied errors when accessing /root/.cache/pip.

The Fix: Use the uid parameter in the mount to map the cache directory to your application user.

# syntax=docker/dockerfile:1
FROM python:3.11-slim

RUN useradd -m appuser
USER appuser
WORKDIR /app

COPY requirements.txt .

# Map the cache to the UID of the appuser
RUN --mount=type=cache,target=/home/appuser/.cache/pip,uid=1000 \
    pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Preventing it

Systemic speed issues require systemic enforcement. Use these strategies to keep your builds lean and fast:

  1. Enforce BuildKit: Ensure your CI environment (GitHub Actions, GitLab CI, etc.) has DOCKER_BUILDKIT=1 enabled. Most modern Docker versions have this on by default, but it is worth checking the runner documentation.
  2. Use .dockerignore: Never copy your local venv or __pycache__ folders into the image. A clean context ensures the cache mount is the only source of truth for packages.
  3. Scan for vulnerabilities: Caching is for speed, not security. Always include a scan step in your CI pipeline using tools like trivy or snyk to ensure the cached packages are not vulnerable.
  4. Multi-stage builds: Always separate your build dependencies (like gcc for compiling C extensions) from your final runtime image. Use the cache mount in the "builder" stage, but ensure your final stage image remains minimal.

By treating the cache mount as a first-class citizen in your Dockerfiles, you eliminate the "wait for packages" tax, allowing your CI/CD pipeline to focus on what actually matters: running tests and verifying your data transformations.

Cover photo by Bernd 📷 Dittrich on Unsplash.


This article was drafted with AI assistance and is intended for technical review and fact-checking before publication.

Top comments (0)