Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Install system dependencies in Docker

Learn how to install system dependencies in Docker containers using apt-get and other package managers. This tutorial covers multi-stage builds, dependency caching, and best practices for minimal images.

Focus: install system dependencies in docker

Sponsored

You finally have a Dockerfile that builds your Python app, but pip install throws gcc: error trying to exec 'cc1': execvp: No such file or directory — or worse, your app crashes at runtime because a shared library is missing. Installing system dependencies in Docker is not as simple as running apt-get install in your base image; every choice adds weight, risk, and complexity. This lesson turns that pain into a repeatable skill — you'll learn where, when, and how to install system packages so your image stays lean, fast, and reliable.

The problem this lesson solves

Docker images are supposed to be minimal, but real applications need system dependencies: compilers, native libraries, database clients, or debugging tools. The default python:3.11-slim image omits many of them to stay small. If you blindly add packages, you bloat the image, increase the attack surface, and waste bandwidth.

Pro tip: Every unnecessary package is a potential vulnerability. A typical apt-get install that you forget to clean up can add 200+ MB to your image.

Without a strategy, developers often:

  • Install packages in the wrong layer, breaking caching
  • Forget to clean up temporary files (/var/lib/apt/lists/*)
  • Use apt-get vs apt inconsistently
  • Install build-time deps in the final stage, bloating production images

The goal: install exactly what your app needs, in the right place, without leaving leftovers.

Core concept / mental model

Think of your Docker image as a bread recipe. The base image is the flour — plain python:3.11-slim. The system dependencies are the yeast and salt — necessary for the bread to work but easy to over-measure. The Dockerfile instructions are the kneading steps.

Multi-stage builds are like having a prep kitchen and a serving kitchen. You do all the messy mixing (compilation) in the prep stage, then pass only the final cooked bread (binary / library) to the serving stage — leaving the mixing bowls and extra ingredients behind.

Key definitions:

  • Build-time dependencies: packages required during image building (compilers, headers, tools like gcc or build-essential).
  • Run-time dependencies: packages your app needs when it actually runs (e.g., libpq5 for PostgreSQL, ffmpeg for media).

Your job is to identify which is which and install accordingly.

How it works step by step

Follow this repeatable sequence every time you need a system dependency:

  1. Identify the dependency. Check the error message or the library documentation. For Python: psycopg2 needs libpq-dev at build time, libpq5 at run time.

  2. Update and install in one RUN command (no separate apt-get update). This prevents caching stale package lists.

  3. Use the --no-install-recommends flag to avoid pulling optional packages.

  4. Clean the apt cache in the same layer with rm -rf /var/lib/apt/lists/*.

  5. Separate build-time and run-time deps with multi-stage builds if possible.

# Stage 1: build-time deps
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install --no-install-recommends -y \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Stage 2: final image
FROM python:3.11-slim
RUN apt-get update && apt-get install --no-install-recommends -y \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

Pro tip: Group all system deps in one RUN block. Each RUN creates a new layer; you want one layer for all apt operations combined.

Hands-on walkthrough

Let's build a Python app that connects to PostgreSQL using psycopg2-binary. The binary package contains precompiled libraries, but sometimes you need the C extension for performance or compatibility.

Example 1: Minimal apt-get install

FROM python:3.11-slim

# Install runtime deps only
RUN apt-get update && \
    apt-get install --no-install-recommends -y \
        libpq5 \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Copy and install Python deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "app.py"]
docker build -t myapp:latest .
# Layers shown: only one apt layer, minimal size

Example 2: Build-time deps via multi-stage

# Build stage
FROM python:3.11-slim AS builder

RUN apt-get update && apt-get install --no-install-recommends -y \
        build-essential \
        libpq-dev \
    && rm -rf /var/lib/apt/lists/*

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

# Final stage
FROM python:3.11-slim AS final

RUN apt-get update && apt-get install --no-install-recommends -y \
        libpq5 \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

WORKDIR /app
COPY . .
CMD ["python", "app.py"]
docker build -t myapp:multistage .
docker images  # Compare size: multistage should be 100-200MB smaller

Example 3: Installing dependencies from a requirements file that includes system packages

Sometimes you need ffmpeg for audio processing in Flask. Use RUN with pip and apt together — but keep them in separate RUN blocks to allow pip's cache independence.

FROM python:3.11-slim

# System deps first
RUN apt-get update && apt-get install --no-install-recommends -y \
        ffmpeg \
        libavcodec-extra \
    && rm -rf /var/lib/apt/lists/*

# Python deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .
CMD ["python", "app.py"]
# Build and run
$ docker build -t audio-app .
$ docker run audio-app python -c "import subprocess; print(subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True).stdout)"
# Output: ffmpeg version 5.1.2 ...

Compare options / when to choose what

Approach When to use Pros Cons
Single-stage apt install Simple apps, only runtime deps Simple to read, one RUN layer Bloat if build deps remain
Multi-stage build Need compilation (gcc, build-essential) Minimal final image More complex Dockerfile
Use binary wheels (e.g., psycopg2-binary) Avoid compilation entirely No sys deps needed No performance patches, limited archs
Docker build arguments (ARG) Different deps for dev vs prod Flexible Requires conditional logic

Rule of thumb: If you only need run-time deps, stay single-stage. If you need compilers or headers, use multi-stage.

Troubleshooting & edge cases

"Package not found" when using slim images

slim images have minimal repositories. You may need to add --no-install-recommends — but if the package is truly missing, consider switching to python:3.11-buster or a full buildpack-deps variant.

Layer caching breaks your installation

If you run apt-get update in its own RUN block, Docker caches it. Then later when you change requirements.txt, apt won't re-run. Always combine update + install + clean in one RUN.

Error: /var/lib/apt/lists/lock or Could not get lock /var/lib/dpkg/lock

This only happens if you run apt-get during image build on a host that uses the same APT database — unlikely. If it happens, check that you aren't running multiple builds simultaneously.

build-essential adds 200MB+ even after cleaning

build-essential pulls many transient packages. Use multi-stage and copy only the compiled artifact (like a .so from pip install --user).

What you learned & what's next

You can now:

  • Explain why and where to install system dependencies in Docker
  • Write a single RUN command that updates, installs, and cleans in one layer
  • Separate build-time and run-time deps using multi-stage builds
  • Avoid common pitfalls like caching problems and unnecessary bloat

Next lesson: Manage environment variables in Docker — you'll inject secrets and configs without leaking them into the image.

Ready to shrink your next Dockerfile? Try refactoring an existing Dockerfile that uses apt-get install in multiple RUN blocks into a single, optimized layer.

Practice recap

Write a Dockerfile for a Python app that connects to MongoDB. Install the minimal runtime dependencies (libssl-dev, ca-certificates) in one optimized layer. Then build and inspect the image size with docker images | grep mongodb-app. Now refactor using multi-stage to install the Python-mongodb driver compiled from source — note the difference in final sizes.

Common mistakes

  • Running apt-get update in a separate RUN line from apt-get install. This means Docker caches the update layer and never refreshes it when you add new packages later.
  • Forgetting --no-install-recommends — this pulls documentation and extra packages that bloat the image unnecessarily.
  • Installing build-essential or similar heavy compile deps in the final stage. Always use multi-stage to keep production images small.
  • Not cleaning apt lists: missing rm -rf /var/lib/apt/lists/* leaves behind index files that add 30-50 MB per layer.

Variations

  1. Use pip install with --no-binary to force compilation against system libraries, coupling your Python dependencies to system packages.
  2. Use apk add in Alpine-based images — smaller but different syntax: apk add --no-cache <package>.
  3. Leverage docker build --build-arg INSTALL_DEV_DEPS=true to conditionally install development dependencies only in dev builds.

Real-world use cases

  • Installing OpenCV system dependencies (libgl1, libglib2.0-0) in a Python image for computer vision inference.
  • Adding Postgres client libraries (libpq-dev) in a multi-stage build to compile psycopg2 for a Flask API.
  • Installing ffmpeg and libx264 in a media-processing service that uses PyAV to transcode video files.

Key takeaways

  • Always combine apt-get update and install in one RUN block to prevent caching issues.
  • Use --no-install-recommends and clean apt lists to keep images small.
  • Separate build-time deps (compilers, headers) from run-time deps using multi-stage builds.
  • Resist the urge to install all packages — only what your app actually needs at runtime.
  • Consider binary wheels (e.g., psycopg2-binary) to avoid system deps entirely when possible.
  • For Alpine images, use apk --no-cache; for Debian/Ubuntu, always use apt-get (not apt).

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.