Install Docker on Raspberry Pi 5: The Setup That Won't Bite You Later
Install Docker CE and Compose v2 on your Pi 5 the right way. Skip the docker.io trap, fix the cgroup gotcha, and ship your first container in 10 minutes.

Install Docker CE and Compose v2 on your Pi 5 the right way. Skip the docker.io trap, fix the cgroup gotcha, and ship your first container in 10 minutes.

Three Pi 5s. Three broken docker.io installs. Same pattern every time — the daemon starts fine, then docker compose up returns command not found because Debian's package doesn't ship the Compose plugin at all. Buildx is missing too. The legacy docker-compose v1 binary it does include can't parse any yaml written after 2021. Debian's repo is months behind Docker CE, ships a half-broken cgroup setup on ARM64, and is the wrong place to get Docker for a Pi 5 in 2026.
By the end of this, you'll have Docker CE and Compose v2 running cleanly on your Pi 5, your first Compose stack on disk, a sane update routine, and every Pi-specific gotcha patched before it bites — the same stack I run on four boards in my house right now.
apt install docker.ioDebian's repos ship a Docker fork that lags the real release by anywhere from 6 months to a year. That's the root of every failure above — the fork loses upstream features faster than Debian's maintainers fold them back in.
Here's what you do instead: pull from Docker's official apt repo. It gets you Docker Engine, the modern CLI, containerd.io, Buildx, and the Compose plugin in one go. Updates land the day Docker ships them. No fork drift, no babysitting.
There's also the convenience script (curl -fsSL https://get.docker.com | sh). It works. I use it for throwaway test boards. Don't use it for anything you want to run for more than a weekend — you give up version pinning and you're stuck with whatever the script picked the day you ran it.
I run the same prep on every fresh Pi 5 before I touch Docker. You should too. It clears out the half-installed wrong packages and gets your OS to a known state.
Confirm you're on 64-bit Bookworm or newer:
uname -m
# expect: aarch64
cat /etc/os-release | grep VERSION_CODENAME
# expect: bookworm (or newer)
If uname -m returns armv7l, you're on 32-bit. Reflash with Raspberry Pi Imager and pick the 64-bit OS. Docker still runs on 32-bit, but you'll hit ARM64-only images constantly and want to throw your Pi out a window. Just go 64-bit.
Then update everything:
sudo apt update
sudo apt full-upgrade -y
sudo reboot
After the reboot, kick out any old Docker bits Debian might have shipped:
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y $pkg
done
If you've never installed any of those, this does nothing. No harm. I run it on every fresh image because I've been burned by phantom containerd packages exactly enough times to make it a habit.
This is the GPG-key-and-sources-list dance. Four commands. Copy them in order.
First, install the prerequisites:
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
Then grab Docker's GPG key and drop it where apt expects it:
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Now add the repo to your sources list. This is the line where most copy-paste guides go sideways — they'll have you use $(lsb_release -cs) which can return weird values on Raspberry Pi OS. Hard-code bookworm instead:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian bookworm stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
When the next Raspberry Pi OS release lands (Trixie), you swap bookworm for trixie. That's the only line you'll ever need to change.
Refresh apt:
sudo apt update
If you see a 403 or a NO_PUBKEY error, you skipped the chmod a+r step. I've done it twice myself. Go back, rerun the chmod, refresh again.
One command. Five packages. The whole stack:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
That gives you the Docker daemon, the CLI, the container runtime, Buildx for multi-arch builds, and docker compose v2 as a CLI plugin (not the legacy docker-compose binary — that one's dead, don't go looking for it). The install also enables and starts the docker systemd service. You don't need to do that manually.
Verify it's alive:
sudo systemctl is-active docker
# expect: active
sudo docker run hello-world
If you get the "Hello from Docker!" message, the engine works. If you get a permission error, your daemon is up but your user isn't in the docker group yet — fix that in the next section.
Typing sudo in front of every Docker command gets old in about an hour. Add your user to the docker group:
sudo usermod -aG docker $USER
Now log out and back in. Or reboot. Don't skip this — group changes don't apply to your current shell, and I've watched people debug "permission denied on /var/run/docker.sock" for 20 minutes because they thought newgrp docker would do it. Sometimes that works. Often it doesn't propagate to child processes. Just log out.
Once you're back in, confirm:
groups
# expect: docker listed in the output
docker run hello-world
# no sudo this time
Security note worth saying out loud: anyone in the docker group can mount your filesystem inside a container and read it as root. On a personal Pi, that's fine. On a shared box, don't add untrusted users to the docker group. Use rootless Docker if you need real isolation between users — that's a separate guide.
This one almost nobody mentions, and it'll bite you the second you try to run a container with --memory or use Compose's mem_limit. Out of the box, Raspberry Pi OS doesn't enable cgroup memory accounting. Docker will run anyway, but memory limits silently do nothing. I lost half a day on this once trying to figure out why a container capped at --memory=512m was happily eating 2GB.
Here's the fix. Edit your kernel command line:
sudo nano /boot/firmware/cmdline.txt
That file is a single line. Don't add a newline. Append these to the end, separated by spaces:
cgroup_memory=1 cgroup_enable=memory
Save, exit, reboot:
sudo reboot
After the reboot, confirm:
docker info | grep -i "warning"
# the WARNING about no swap limit support should be gone
If you don't plan to use memory limits, you can skip this. Every Compose stack I've ever shipped uses them. Set it now, save your future self the debugging.
Before you publish a port, grab your Pi's IP address:
hostname -I
# example output: 192.168.1.50
Note that down. That's the address you'll hit from any other device on your network whenever a container exposes a port.
hello-world proves Docker works. It doesn't prove your stack works. Pull a real image — I use lightweight nginx:alpine for a quick sanity check:
docker run -d --name test-nginx -p 8080:80 nginx:alpine
Open http://<your-pi-ip>:8080 in any browser on your network. You should see the nginx welcome page. Tear it down:
docker stop test-nginx && docker rm test-nginx
Here's the ARM64 trap. Your Pi 5 runs linux/arm64. A big chunk of Docker Hub images are linux/amd64 only. When you pull one of those, you'll get:
exec format error
That error means "this binary isn't built for your CPU." Before you pull anything new, check the image's tags on Docker Hub for arm64 or linux/arm64. Official images (nginx, postgres, redis, node, python, mariadb) almost always have arm64 builds. Random community images often don't. When you find one that doesn't, look for a fork that does — or build it yourself with Buildx, which you already have.
docker run. Start Writing Compose.I haven't typed a raw docker run outside of one-off tests in two years. Every container I want to keep lives in a docker-compose.yml because the day I rebuild this Pi — and I will, sooner than I want to — I want the whole stack back up with one command. Not 14 forgotten flags I have to reconstruct from bash history.
Here's the workflow. Make a folder for your Docker apps, one subfolder per stack:
mkdir -p ~/docker/nginx-test
cd ~/docker/nginx-test
Every stack gets its own folder. ~/docker/pihole/, ~/docker/jellyfin/, ~/docker/homeassistant/ — the pattern locks in fast. Tar that one folder, you've backed up the whole stack's config.
Create the Compose file:
nano docker-compose.yml
Paste this:
services:
nginx:
image: nginx:alpine
container_name: nginx-test
ports:
- "8080:80"
restart: unless-stopped
Save (Ctrl-O, Enter, Ctrl-X). Bring it up:
docker compose up -d
The -d is detached mode — runs in the background. Without it, the container's logs hijack your terminal and Ctrl-C kills the container. You'll forget the -d once. Everyone does.
Check it's alive:
docker compose ps
Tear it down when you're done:
docker compose down
restart: unless-stopped in the yaml means the container comes back automatically on reboot. Set that on every container you actually want running 24/7. Skip it on test stuff, set it everywhere else.
I update my Pi 5 stack on a slow Sunday with coffee. Two commands per app, five minutes total. Here's the routine.
For any Compose stack:
cd ~/docker/app-name
docker compose pull
docker compose up -d
pull grabs the latest image tags. up -d recreates any container whose image changed. Containers that didn't change keep running untouched. Then clean up the old images so they don't eat your disk:
docker image prune -f
For Docker itself — since you installed from the official apt repo, engine updates ride apt like every other package:
sudo apt update && sudo apt upgrade -y
Restart the daemon only if the upgrade specifically tells you to. Containers with restart: unless-stopped survive engine restarts without you doing anything.
One thing worth knowing: if you pin image tags like nginx:1.27 instead of nginx:latest (and you should — latest is a roulette wheel), docker compose pull won't grab new major versions automatically. You bump the tag in the yaml when you decide it's time. That's a feature, not a bug. Production stacks shouldn't update silently.
Forget memorizing every Docker flag. These are the ones you'll type weekly. Keep them handy.
Looking at what's running and what's not:
docker ps # running containers
docker ps -a # all containers, including stopped
docker images # downloaded images
docker stats # live CPU and memory per container
Reading logs and shelling in:
docker logs nginx-test # recent logs
docker logs -f nginx-test # follow live (Ctrl-C to exit)
docker exec -it nginx-test sh # shell inside the container
Container lifecycle:
docker stop nginx-test
docker start nginx-test
docker restart nginx-test
docker rm nginx-test # only works when it's stopped
Removing images and reclaiming disk:
docker rmi nginx:alpine # remove a specific image
docker system df # what's eating your disk
docker image prune # unused images
docker container prune # stopped containers
docker volume prune # unused volumes — careful, this nukes data
docker system prune -a # nuclear option, removes everything unused
I run docker system df first, look at what's actually taking space, then prune only what I'm sure I can lose. Never run docker system prune -a against a stack with volumes you care about without backups in hand.
I've watched two SD cards die under Docker write loads. Both were "high-endurance" cards. The Pi 5 is fast enough that the bottleneck stops being the CPU and starts being the SD card's write endurance — every container log line, every database commit, every restart counter hits the same flash cells over and over.
Two things you do tonight to extend the card's life.
First, cap Docker's log files. By default, container logs grow until they fill the disk. Edit (or create) /etc/docker/daemon.json:
sudo nano /etc/docker/daemon.json
Paste:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Restart Docker:
sudo systemctl restart docker
Now each container caps logs at 10MB × 3 files. Existing containers won't pick this up until you recreate them — docker compose up -d on each stack does that.
Second, plan for SSD. The Pi 5 is fast enough that running Docker workloads off USB 3.0 SSD or an NVMe HAT changes the experience completely. Two paths:
/var/lib/docker to the SSD.Do the first one. It's simpler and the OS gets the same write-endurance win as Docker. If you're already running on SD and don't want to reflash today, at minimum order an SSD now so it's on hand when the card starts throwing errors. It will.
Six problems will hit you in your first month. Here's what they look like and how you unstick yourself fast.
permission denied while trying to connect to /var/run/docker.sock
Your user isn't in the docker group, or the group change hasn't taken effect. Run groups — if docker isn't in the output, run sudo usermod -aG docker $USER and reboot. If docker is there but you still get the error, you logged in before the group change took effect. Reboot, or at minimum fully log out and back in.
exec format error when starting a container
The image you pulled isn't built for ARM64. Check Docker Hub for the image's Tags page and look for linux/arm64 in the supported architectures. If it's not there, find a fork that supports arm64 or build your own with Buildx. Don't try to force amd64 images on the Pi — they don't run.
Error: bind: address already in use
Something else on the Pi is already holding that port. Find what:
sudo ss -tulpn | grep :8080
Either stop the offending process or pick a different host port in your Compose file (change "8080:80" to "8081:80").
Container starts, then stops a second later
Read the logs:
docker logs <container-name>
99% of the time the answer is in the last 20 lines — a missing env var, a config file in the wrong place, a permission issue on a mounted volume. The other 1% is the container's main process exiting cleanly, which Docker treats as a stop. Check the image's docs for what it expects as the long-running command.
E: Unable to locate package docker-ce
The apt repo step didn't take. Verify your sources file:
cat /etc/apt/sources.list.d/docker.list
If it's empty or wrong, redo the "Add Docker's Official Repo" section, make sure the GPG key has read permissions (sudo chmod a+r /etc/apt/keyrings/docker.asc), then sudo apt update again.
no space left on device when pulling an image
Your disk is full or close to it. Check both views:
df -h /
docker system df
If Docker is showing GBs of unused images, docker image prune -a reclaims them. If / is full but Docker is showing little, the disk hog is somewhere else — sudo du -sh /var/log/* is a good next look. Clean what's safe to clean, and use this as the prompt to actually order that SSD.
Three things, in order:
~/docker/<name>/docker-compose.yml. No raw docker run flags. You'll thank yourself in six months when you migrate the stack to a different box or rebuild after an SD card death.~/docker plus /var/lib/docker/volumes before you put anything important in either. The first time you rebuild your card without that backup will be the last.I've run this exact install on four Pi 5s, and the only one that ever gave me real trouble was the one where I skipped the cgroup line and then tried to debug memory limits at midnight. Don't be me. Set the cgroup line tonight, log out and back in to pick up your docker group membership, ship your first Compose stack before bed. The Pi 5 is fast enough now that Docker on it isn't a toy — it's a real homelab in a box that fits in your pocket.