Why not the distribution package?
Ubuntu's own docker.io package works, but it lags behind upstream and does not include the current Compose plugin. Docker's repository gives you current Engine, Buildx and Compose releases with security fixes delivered through normal apt upgrades.
1. Remove conflicting packages
for p in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt remove -y $p 2>/dev/null
done
2. Add Docker's repository
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
On Debian, replace ubuntu with debian in both URLs.
3. Install Engine and Compose
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker run --rm hello-world
docker compose version
4. Decide about the docker group
Adding your user to the docker group lets you run commands without sudo:
sudo usermod -aG docker $USER
Be aware that membership is effectively root access, because anyone in the group can start a container that mounts the host filesystem. On a single-admin server that is usually acceptable; on shared servers, keep using sudo.
5. Rotate container logs
By default Docker keeps container logs forever, and a chatty container can fill the disk. Cap them globally:
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
sudo systemctl restart docker
The limits apply to containers created after the restart.
6. A Compose file that survives reboots
Create ~/app/compose.yaml:
services:
web:
image: nginx:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
volumes:
- dbdata:/var/lib/postgresql/data
secrets:
db_password:
file: ./db_password.txt
volumes:
dbdata:
Start it with docker compose up -d. Note the port binding: 127.0.0.1:8080 keeps the container private to the host. Put nginx with Let's Encrypt in front for public HTTPS. Binding to localhost matters because Docker's published ports bypass UFW rules, as explained in our UFW guide.
7. Housekeeping
docker compose pull && docker compose up -d # update images
docker system df # see what uses space
docker image prune -a --filter "until=168h" # remove unused images older than a week
Schedule the prune with a weekly cron job on build-heavy servers.
Sizing
A couple of small containers run happily on Small. Five to ten services with a database fit Medium, and CI runners building images benefit from the extra cores of Pro.
