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.
- Simplest fix: use
python:3.12-slim(Debian, glibc). Wheels install directly, and the image is typically only slightly larger. - 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. - Install runtime libraries in the final stage:
apk add --no-cache libpq jpeg zlib libffi. - Prefer packages with musllinux wheels, such as
psycopg[binary]3.x, recent Pillow, and cryptography. - 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.
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.
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
searchdomain 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
- Cut size with techniques that work on any base: Optimize Django Docker Image Size.
- For psycopg build errors on any base, see Fix psycopg Install Errors in Django.
- Start from a proven Dockerfile with Dockerize Django for Production.
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.