Chapter 6 of Be Your Own Cloud — and Chapter 9 of The Privacy Diet — give you the one-liner:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for the group change to apply, then verify:
docker run hello-world That works. This walkthrough covers what it does, why the apt install docker.io you’ll find elsewhere is the wrong package, what adding yourself to the docker group really grants, and the maintenance routine that stops a homelab becoming a pile of unpatched containers.
Don’t use the distro package
Three things claim to be Docker on Ubuntu:
| Source | What you get |
|---|---|
apt install docker.io | Ubuntu’s own build. Often behind Docker’s releases, with Compose v2 in a separate package. Docker asks you to remove it before installing theirs. |
snap install docker | Confined. Bind-mounting host paths fails in ways that waste an afternoon. |
| Docker’s official apt repo | Current docker-ce, the Compose v2 plugin, security fixes on Docker’s schedule. |
Use the official repository. The get.docker.com script simply sets it up for you — here it is done explicitly, so you can see what lands on the machine:
sudo apt update
sudo apt install -y ca-certificates curl
# Docker's signing key, stored where apt expects a keyring
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# The repo, pinned to that key and your release
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
This is the block from Docker’s own install page, which supports Ubuntu 22.04, 24.04 and 26.04 LTS. Note Signed-By: — it scopes that key to this repository only, so Docker’s key can’t vouch for packages from anywhere else. (Older guides write a one-line docker.list instead; it does the same job.)
The docker group, honestly
To run docker without sudo:
sudo usermod -aG docker $USER
newgrp docker # or log out and back in
Be clear about what you just did. The Docker daemon runs as root, and group membership grants unrestricted access to its socket. Anyone in the docker group can start a container that mounts / and read or write anything on the host:
# Don't run this. It is what group membership permits.
docker run -v /:/host -it alpine chroot /host
docker group membership is root access, without a password prompt or a sudo log entry. On a single-admin home server that’s a reasonable trade for convenience — you are root. On anything shared, use rootless mode instead.
Verify:
docker run --rm hello-world
docker compose version # v2 is a plugin: "docker compose", not "docker-compose"
Lay out the filesystem before you deploy anything
Decide now, or you’ll be hunting for volumes in six months. Be Your Own Cloud uses one folder per service under /opt/stacks:
sudo mkdir -p /opt/stacks
sudo chown $USER:$USER /opt/stacks /opt/stacks/
vaultwarden/
docker-compose.yml
.env
data/
nextcloud/
docker-compose.yml
.env
pihole/
docker-compose.yml
.env Each folder holds that service’s docker-compose.yml, its .env and its data. Self-contained, obvious to back up, trivial to move to another machine. When you reach backups (Chapter 18), /opt/stacks is the path you point Restic at.
A Compose file worth reading
Here’s the book’s Vaultwarden file from Chapter 11 — the corrected version of the one in The Privacy Diet:
services:
vaultwarden:
image: vaultwarden/server:1.37.3
container_name: vaultwarden
restart: unless-stopped
volumes:
- ./data:/data
environment:
# set in .env: true now, false once your account exists
- SIGNUPS_ALLOWED=${SIGNUPS_ALLOWED}
- ADMIN_TOKEN=${ADMIN_TOKEN}
networks:
# Chapter 8's shared network -- no host port published at all
- proxy
networks:
proxy:
external: true Four things about it are deliberate:
No version: line. The top-level version key is obsolete in Compose v2 and only produces a warning.
The image tag is pinned. :latest means a pull can silently move you across a breaking change, and you can’t tell what’s running or roll back to what worked. Pin the version, bump it on purpose.
Secrets come from .env. ${ADMIN_TOKEN} is filled in from the .env file beside the compose file, so the compose file itself is safe to share (Chapter 9 of Be Your Own Cloud).
No ports: at all. Without a host address, a line like '8080:80' publishes on 0.0.0.0 — every interface — and, because Docker writes its own iptables rules, ufw will not stop it. This surprises people badly. Vaultwarden avoids the problem entirely: it publishes nothing and joins the shared proxy network from Chapter 8, so the reverse proxy is the only way in. Check what you’re actually exposing:
sudo ss -tlnp | grep docker
Start it (the Vaultwarden tutorial has the .env and proxy steps):
cd /opt/stacks/vaultwarden
docker compose up -d
docker compose logs -f # Ctrl-C stops following, not the container
Updating without breaking things
Pinned tags mean updates are a choice. The routine, per service:
cd /opt/stacks/vaultwarden
# 1. back up first — always
# 2. edit docker-compose.yml, bump the tag
docker compose pull
docker compose up -d
docker compose logs --tail=50
Chapter 27 of Be Your Own Cloud turns this into a routine, with a check that last night’s backup ran before you touch anything.
Clear out accumulated layers occasionally. Know what you’re typing:
docker image prune -a # unused images. Safe.
docker system prune # + stopped containers, unused networks, build cache.
docker system prune --volumes # ALSO DELETES UNUSED VOLUMES. Data loss. Avoid.
Most of the book’s services keep their data in bind-mounted folders like ./data, which no prune command touches — one more reason for the layout above. A few use named volumes on purpose (Nextcloud AIO keeps its database in them), so leave --volumes off.
Watchtower, and why not to automate this
Watchtower was the popular tool for updating containers automatically, and the original project is no longer maintained: its repository has been archived. Whatever you’d use in its place, on a homelab, don’t. Unattended updates to packages are good — they’re narrow and heavily tested. Unattended updates to application containers means a breaking schema migration happens while you’re asleep and you find out when your password manager won’t load. Update deliberately, after a backup.
Where this leaves you
Current Docker from the right repository and a predictable filesystem layout. The next thing to build is the reverse proxy from Chapter 8 of Be Your Own Cloud — Reverse Proxy and HTTPS — so services get real hostnames and certificates instead of port numbers — then the services themselves, starting with Pi-hole and Unbound.
Stuck on a step? Get in touch.