Docker on Raspberry Pi 4: Install, Build ARM64 Images and Use Compose
Once a Raspberry Pi 4 runs more than one service, installing everything directly on the OS gets messy: conflicting Python versions, leftover config files and upgrades that break each other. Docker packages each service with its own dependencies into a container. Containers start in seconds, can be removed cleanly, and the same docker-compose.yml file rebuilds your whole setup on a fresh SD card or SSD.
In this tutorial you will install Docker Engine, understand the ARM64 image question, write a Dockerfile for a small Python service, and run it alongside a Redis database using Docker Compose with persistent volumes.
Key Concepts
| Term | Meaning |
|---|---|
| Image | A read-only template containing an app and its dependencies |
| Container | A running instance of an image, isolated from other processes |
| Volume | Storage managed by Docker that survives container removal |
| Port mapping | Connects a host port to a container port, for example 8080:80 |
| Compose file | YAML that defines several containers, networks and volumes together |
| Architecture | Images are built per CPU type; the Pi 4 on 64-bit OS needs linux/arm64 |
The architecture point matters most. Most popular images on Docker Hub are multi-architecture and automatically pull the arm64 variant. An image built only for amd64 fails on the Pi with an exec format error. Check an image's supported platforms on its Docker Hub tags page before relying on it. This is also why you should use the 64-bit Raspberry Pi OS: support for 32-bit ARM images is shrinking.
Installing Docker Engine
Docker's convenience script detects Raspberry Pi OS and installs Docker Engine, the CLI, containerd and the Compose plugin from Docker's own repository. Download it first and read it if you want to see what it does:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh # Let your user run docker without sudo (log out and back in afterwards) sudo usermod -aG docker $USER # Verify docker version docker compose version docker run --rm hello-world
The Docker service is enabled at boot by the install. Avoid the older docker.io and docker-compose packages from Debian's repository alongside this install; mixing them causes version conflicts.
Building Your Own Image
The example service is a visit counter: a Flask app that increments a number stored in Redis. Create a folder ~/counter containing app.py, requirements.txt, Dockerfile and docker-compose.yml.
# app.py - visit counter; Redis host name comes from the compose service name
import os
import socket
from flask import Flask
import redis
app = Flask(__name__)
# 'redis' resolves to the Redis container on the compose network
db = redis.Redis(host=os.environ.get('REDIS_HOST', 'redis'), port=6379)
@app.route('/')
def hello():
count = db.incr('hits') # atomic increment, survives restarts
return f'Visit number {count} served by container {socket.gethostname()}\n'
# ~/counter/requirements.txt flask==3.0.3 redis==5.0.8 gunicorn==22.0.0
# Official Python images are multi-arch, so this pulls the arm64 variant on a Pi 4
FROM python:3.11-slim
# Do not write .pyc files; send logs straight to docker logs
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies first so this layer is cached between code changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY app.py .
# Run as an unprivileged user inside the container
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:8000", "app:app"]
services:
web:
build: . # build from the Dockerfile in this folder
ports:
- "8080:8000" # host port 8080 -> container port 8000
environment:
REDIS_HOST: redis
depends_on:
- redis
restart: unless-stopped # come back after a reboot or crash
redis:
image: redis:7-alpine # multi-arch image, arm64 pulled automatically
command: ["redis-server", "--appendonly", "yes"] # persist to disk
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data: # named volume survives 'docker compose down'
Running and Managing the Stack
| Command | What it does |
|---|---|
| docker compose up -d --build | Build the web image and start both containers in the background |
| docker compose ps | List the stack's containers and their state |
| docker compose logs -f web | Follow the web container's output |
| docker compose down | Stop and remove containers; the named volume is kept |
| docker system df | Show disk space used by images, containers and volumes |
| docker image prune | Remove unused dangling images to reclaim space |
Open http://pi4lab.local:8080/ and refresh a few times; the counter increases. Run docker compose down and then up -d again: the count continues, because Redis stores its data in the redis-data volume rather than inside the container.
Updating Images Safely
Containers never update themselves. To move to newer versions, run docker compose pull to fetch updated images, then docker compose up -d to recreate only the containers whose image changed. Pin versions in production, for example redis:7-alpine rather than redis:latest, so an update never jumps to a new major version without you choosing it. Before updating a service that stores important data, back up its volume. A simple method is to stop the stack and archive the volume's contents with a temporary container: docker run --rm -v counter_redis-data:/data -v $PWD:/backup alpine tar czf /backup/redis.tgz -C /data . (Compose prefixes volume names with the project folder name, which docker volume ls will confirm).
Troubleshooting
- exec format error: the image has no arm64 build. Find a multi-arch alternative or build it yourself from its Dockerfile on the Pi.
- permission denied on /var/run/docker.sock: you have not logged out and back in since joining the docker group.
- SD card fills up: images and build cache grow quickly. Use
docker system dfand prune regularly, or move to an SSD as described in the USB boot article. - Container cannot reach redis: use the compose service name as the host name, not
localhost; each container has its own network namespace. - Slow builds: pip may compile packages from source when no ARM64 wheel exists. Prefer libraries that publish aarch64 wheels.
Summary
Install Docker with the official script on 64-bit Raspberry Pi OS, add your user to the docker group, and check that images support arm64. Write a Dockerfile that installs dependencies before copying code for faster rebuilds, and describe multi-container setups in docker-compose.yml with named volumes for data that must persist. The same pattern is used to run Home Assistant and other services later in this series.