Docker Engine on Debian
Docker Engine and the Compose plugin on a headless Debian 13 box, with storage on a separate data volume and DNS that works behind Tailscale.
Names and addresses are placeholders. The list says what each one stands for.
What it does here
Every service on the box runs as a container. Docker Engine with the Compose v2 plugin is the only runtime on the host, so the host itself stays small and replaceable.
Canonical setup
This follows Docker's own Debian instructions. Use the official repository, not Debian's docker.io package, so Compose v2 and Buildx come along and versions track upstream.
sudo install -m 0755 -d /etc/apt/keyrings
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
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt updatesudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-pluginsudo usermod -aG docker youruser
sudo systemctl enable --now dockerUpstream: Install Docker Engine on Debian and Linux post-installation steps.
In this build
- Storage moved to /srv/data/dockerImages and volumes live on the data disk, so an OS reinstall leaves them alone.
- DNS pinned to 1.1.1.1 and 1.0.0.1Bridge containers can't reach Tailscale's MagicDNS resolver. See the MagicDNS seam below.
- Logs capped at 3 × 10 MB per containerDefault json-file logs grow without limit and fill the disk quietly.
- Docker waits for the /srv mountWithout it, Docker can start before the data volume is mounted.
Changes 1 to 3 are one file. It's root-owned, so write it with sudo.
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
"data-root": "/srv/data/docker",
"dns": ["1.1.1.1", "1.0.0.1"],
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
EOFChange 4 is a systemd drop-in that orders Docker after the mount.
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/wait-for-srv.conf \
>/dev/null <<'EOF'
[Unit]
RequiresMountsFor=/srv/data/docker
EOF
sudo systemctl daemon-reload
sudo systemctl restart dockerSeams
Where this meets the rest of the build. Each one leads with what you will actually see.
Symptomserver misbehaving
A bridge-networked container inherits the host's resolver, which on a tailnet is 100.100.100.100. That address isn't reachable from inside the container, so external lookups fail. Pinning dns in daemon.json fixes every bridge container at once. Host-network containers use the host resolver directly and are unaffected.
Symptomno error, and no containers after a reboot
With data-root on /srv, Docker can win the race at boot and start against an empty mountpoint. The RequiresMountsFor drop-in closes the race. Only a reboot test catches this one.
Symptompermission denied while trying to connect to the Docker daemon socket
Group membership applies to new login sessions only. Run newgrp docker for the current shell, or log out and back in.
Symptomthe same error, in panes opened after newgrp
A tmux server started before the group change keeps the old groups, and every new pane inherits them, even after you reconnect. Run tmux kill-server once. Containers keep running.
Verify
docker run --rm hello-world
docker info | grep "Docker Root Dir" # /srv/data/docker
id -nG # includes dockerThen reboot and confirm docker ps shows your containers without intervention.
- 17 Oct 2026First published. Verified on Debian 13.6.