Why Django Docker Builds Fail on Alpine, and How to Fix Them

Fix Django Docker builds that fail or crawl on python:alpine: understand musl and missing wheels, install the right build packages for psycopg, Pillow, and cryptography, or switch to python:slim for faster, smaller, more reliable images.

Problem statement

You switched the Dockerfile to python:3.12-alpine to get a smaller image. Now the build fails:

error: command 'gcc' failed: No such file or directory
...
Error: pg_config executable not found.
...
The headers or library files could not be found for jpeg,
a required dependency when compiling Pillow from source.

Or it succeeds but takes 15 minutes instead of 1, and the final image is not much smaller. Alpine uses musl libc instead of glibc. Many Python packages publish prebuilt wheels for glibc (manylinux) but fewer for musl (musllinux), so pip falls back to compiling from source, which needs a compiler, headers, and time.

Why Alpine builds are slowAlpine basemusl libcpip looksfor musllinux wheelsNone availablebuild from sourceNeeds compilersslow or failsWithout matching wheels, every compiled package must be built inside your image.
  1. Simplest fix: use python:3.12-slim (Debian, glibc). Wheels install directly, and the image is typically only slightly larger.
  2. If you must use Alpine, install build dependencies in a builder stage: apk add --no-cache build-base postgresql-dev jpeg-dev zlib-dev libffi-dev.
  3. Install runtime libraries in the final stage: apk add --no-cache libpq jpeg zlib libffi.
  4. Prefer packages with musllinux wheels, such as psycopg[binary] 3.x, recent Pillow, and cryptography.
  5. Measure before choosing: compare final sizes with docker image ls.

Step-by-step solution

1. Decide whether Alpine is worth it

For a Django app, the base size difference is small once dependencies are installed:

docker image ls --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep myapp

The slim image ships glibc and compatible wheels, so builds are fast and predictable. Alpine saves tens of megabytes on the base but often loses them back to build tools or compiled artifacts, and musl has subtle differences in DNS resolution, locale handling, and thread stack sizes.

which base to choosepython:3.12-alpine✓smallest base layer✕fewer prebuilt wheels✕musl behavior differencespython:3.12-slim✓manylinux wheels install✓fast, repeatable builds✓final size close to AlpineFor most Django apps, slim gives the better tradedjango-deployment.com
Measure your own image; the answer depends on which compiled packages you use.

2. Switch to slim

FROM python:3.12-slim AS app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

With psycopg[binary], Pillow, and cryptography all shipping manylinux wheels, this needs no compiler at all.

3. If you stay on Alpine, use a builder stage

FROM python:3.12-alpine AS build
RUN apk add --no-cache build-base postgresql-dev jpeg-dev zlib-dev libffi-dev
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-alpine
RUN apk add --no-cache libpq jpeg zlib libffi
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
WORKDIR /app
COPY . .

Build tools stay in the first stage; the final image only has runtime libraries. Missing a runtime library shows up as ImportError: Error loading shared library libpq.so.5 at startup, not during the build.

4. Check which packages lack musl wheels

pip download --only-binary=:all: --platform musllinux_1_2_x86_64 \
  --python-version 3.12 -r requirements.txt -d /tmp/wheels

Packages that fail this command will compile from source on Alpine. Upgrading them often helps, since musllinux wheels became common only in recent years.

error and missing pieceErrorMissing piecegcc not foundcompiler; apk addbuild-base in builderpg_config not foundpostgresql-dev, or psycopg[binary] 3.xPillow needs jpegjpeg-dev zlib-dev tobuild; jpeg zlib to runlibpq.so.5 at runtimeruntime lib missing; apk add libpqBuild headers go in the builder stage, shared libraries in the final onedjango-deployment.com
Every compiled package adds a pair: a -dev package to build and a library to run.

Explanation

Python wheels with compiled code are tied to a libc. The manylinux standard targets glibc, used by Debian, Ubuntu, and Red Hat. Alpine's musl is a different C library, so manylinux wheels cannot be used and pip looks for musllinux wheels instead. When none exist for your package and Python version, pip downloads the source distribution and compiles it, requiring headers for every C library involved. That is the source of both the failures and the slow builds.

Edge cases / notes

  • Performance. CPython on musl can be slower in some workloads, partly because of musl's memory allocator. Benchmark if you are latency sensitive.
  • DNS. musl resolves names differently than glibc (no search domain retries in older versions), which matters in Kubernetes. Recent musl versions improved this.
  • Distroless alternative. If size is the goal, a slim builder with a distroless runtime image can be smaller than Alpine while keeping glibc.

Next steps

FAQ

Is Alpine bad for Python?

Not bad, but it often loses its size advantage once compiled dependencies are installed, and builds take longer. Slim is the safer default.

Why is my Alpine build so slow?

pip is compiling packages from source because no musl wheels match. Each compiled package can take minutes.

Can I use psycopg2-binary on Alpine?

Recent psycopg2-binary releases publish musllinux wheels for common Python versions. Psycopg 3 with the binary extra is the modern choice.

How much smaller is Alpine in practice?

For a typical Django app with Postgres and Pillow, the difference is often small, sometimes in favor of slim once build artifacts are counted.

Keep going