Build Multi-Architecture Django Docker Images
Build Django images for both amd64 and arm64 (Graviton, Ampere, Apple Silicon) with docker buildx: QEMU versus native runners, wheels, caching in GitHub Actions, and verifying manifests.
Problem statement
ARM64 servers (AWS Graviton, Hetzner and Oracle Ampere instances, and others) are often cheaper per unit of performance than x86 machines, and developers on Apple Silicon laptops build ARM images by default. A Django image built for one architecture fails on the other with exec format error, or runs slowly under emulation.
A multi-architecture image contains a variant for each platform under one tag; Docker pulls the right one automatically. This guide builds such images for Django, handles Python packages with native extensions, and keeps CI build times reasonable.
- Use
docker buildxwith a builder that supports multiple platforms. - Build with
--platform linux/amd64,linux/arm64 --push; multi-platform results must go to a registry (or a store that supports them). - Prefer base images and Python packages with prebuilt wheels for both architectures (
python:3.12-slim,psycopg[binary]or build dependencies forpsycopg[c]). - In GitHub Actions, use
setup-qemu-action,setup-buildx-action, andbuild-push-actionwithplatforms:; for faster builds, use native ARM runners and merge the results. - Cache layers (for example
type=gha) to avoid rebuilding dependencies on every run. - Verify with
docker buildx imagetools inspect.
Step-by-step solution
1. Make the Dockerfile architecture-neutral
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
USER nobody
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
Official Python images are published for both platforms, so FROM python:3.12-slim resolves to the right variant automatically. Things that break multi-arch builds:
- Downloading architecture-specific binaries (for example a CLI tool) with a hardcoded
amd64URL. Use theTARGETARCHbuild argument:
ARG TARGETARCH
RUN curl -fsSL -o /usr/local/bin/tool "https://example.com/tool-linux-${TARGETARCH}" && chmod +x /usr/local/bin/tool
- Packages without ARM wheels, which then compile from source. Most mainstream Django dependencies ship
manylinuxwheels foraarch64. If a package compiles, add the build tools in a builder stage and copy the installed packages into the runtime stage.
2. Build locally
docker buildx create --name multi --use
docker buildx inspect --bootstrap
docker buildx build --platform linux/amd64,linux/arm64 \
-t ghcr.io/example/myapp:1.4.0 --push .
If the builder cannot run the foreign platform, register QEMU emulators first:
docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
A plain docker build (or --load) produces a single-platform image for the local architecture. Multi-platform builds are pushed to a registry, or loaded into a local image store only when your Docker setup uses the containerd image store.
3. Build in GitHub Actions with QEMU
# .github/workflows/image.yml
name: image
on:
push:
branches: [main]
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/example/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
QEMU emulation is simple but slow for CPU-heavy steps: compiling C extensions or running collectstatic with heavy post-processing can take several times longer under emulation.
4. Faster: native runners and a merged manifest
Build each platform on a native runner, push by digest, then combine:
jobs:
build:
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
# checkout, setup-buildx, login as above
- id: build
uses: docker/build-push-action@v6
with:
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/example/myapp,push-by-digest=true,name-canonical=true,push=true
- run: echo "${{ steps.build.outputs.digest }}" > digest-${{ strategy.job-index }}.txt
- uses: actions/upload-artifact@v4
with: { name: "digest-${{ strategy.job-index }}", path: "digest-*.txt" }
merge:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { pattern: "digest-*", merge-multiple: true }
# login as above
- run: |
docker buildx imagetools create -t ghcr.io/example/myapp:${{ github.sha }} \
$(for f in digest-*.txt; do echo "ghcr.io/example/myapp@$(cat $f)"; done)
Check the runner labels available to your account; ARM runners for public and private repositories have different availability.
5. Verify the result
docker buildx imagetools inspect ghcr.io/example/myapp:1.4.0
Expect a manifest list with entries for linux/amd64 and linux/arm64. Test both variants actually run your app:
docker run --rm --platform linux/arm64 ghcr.io/example/myapp:1.4.0 python -c "import django, psycopg; print(django.get_version())"
docker run --rm --platform linux/amd64 ghcr.io/example/myapp:1.4.0 python manage.py check
Verification check:
imagetools inspectlists both platforms.- On an ARM server,
docker image inspect --format '{{.Architecture}}' ghcr.io/example/myapp:1.4.0printsarm64. - The app passes
manage.py check --deployon both.
Explanation
A multi-arch tag points to an OCI image index (manifest list) that references one image per platform. When a host pulls the tag, the registry client selects the image matching the host's OS and CPU. buildx builds each variant, either natively or through QEMU emulation, and pushes them together with the index. Because the Dockerfile is shared, the only architecture-specific parts are binaries you download and packages that must be compiled, which is where most multi-arch problems come from.
Edge cases / notes
Performance differences
Benchmark your app on ARM before switching production; most pure-Python and database-bound Django workloads perform well, but some native libraries have less optimised ARM builds.
Local development
Apple Silicon developers building images for x86 servers should build with --platform linux/amd64 explicitly or let CI build release images. Pushing an ARM-only image from a laptop is a classic cause of exec format error in production.
When to automate this
Build release images only in CI, never from laptops, and tag them with the commit SHA. A post-build job that runs manage.py check inside each platform variant catches architecture-specific dependency problems before deploy.
Next steps
- Start from a solid base with How to Dockerize a Django App for Production.
- Keep each variant lean with Build Smaller, Faster Django Docker Images.
- Wire the build into deploys with Deploy Django with GitHub Actions CI/CD.
- If PostgreSQL drivers fail to build on ARM, see Fix psycopg Installation Errors on Django Servers.
FAQ
What does "exec format error" mean for a Django container?
The image was built for a different CPU architecture than the host. Build a multi-arch image or one for the host's platform.
Can Django run on ARM64 servers?
Yes. Python, Django, and common dependencies such as psycopg, Pillow, and cryptography provide ARM64 wheels.
Why is my multi-arch build so slow in CI?
The non-native platform is built under QEMU emulation. Use native ARM runners per platform and merge the results into one tag.
Can I load a multi-platform image into local Docker?
Only with an image store that supports multi-platform images, such as Docker's containerd image store. Otherwise push to a registry or build one platform with --load.