1. Install Docker Engine and Compose
On Ubuntu (for Debian, replace ubuntu with debian in both URLs):
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 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/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo docker run --rm hello-world
To run docker without sudo, add yourself to the docker group and log in again. Be aware that membership of that group is equivalent to root on the server.
sudo usermod -aG docker $USER
2. Cap Log Sizes Before You Start Anything
By default each container logs to a JSON file with no size limit. Switch the default to the local driver, which rotates and compresses, then restart Docker:
echo '{ "log-driver": "local", "log-opts": { "max-size": "20m", "max-file": "5" } }' \
| sudo tee /etc/docker/daemon.json
sudo systemctl restart dockerThis applies to containers created from now on, which is why it comes first.
3. Describe a Stack with Compose
One folder per stack, with a compose.yaml in it:
mkdir -p ~/apps/whoami && cd ~/apps/whoami nano compose.yaml
services:
web:
image: traefik/whoami
restart: unless-stopped
ports:
- "127.0.0.1:8080:80" # reachable only from the server itselfdocker compose up -d docker compose ps curl http://127.0.0.1:8080 docker compose logs -f web
restart: unless-stopped brings the service back after a crash or a reboot.
4. Published Ports Skip UFW
This is the one that catches everyone. A mapping like "8080:80" listens on every address, and Docker inserts firewall rules for it that are checked before UFW's — so the port is open to the internet even if ufw status says otherwise. Two safe patterns:
- Publish on
127.0.0.1as above and put Nginx or Caddy in front for HTTPS — see hosting a website, step 6. - Publish without an address only for ports you genuinely want public, such as 80 and 443 on a proxy container.
sudo ss -tlnp shows what is listening on the server. To see what the internet can actually reach, test from another machine, for example nc -zv 203.0.113.10 8080.
The firewall on your server's page in the dashboard is applied outside the server, before traffic reaches it, so Docker's rules cannot bypass it. Allowing only the ports you mean to expose there is a dependable backstop.
5. Updating and Cleaning Up
cd ~/apps/whoami docker compose pull # fetch newer images docker compose up -d # recreate what changed docker image prune -f # drop the old images docker system df # what is using the disk
6. Back Up the Data, Not the Containers
Everything worth keeping lives in volumes or bind mounts, plus the compose files. Keep bind-mounted data under one folder (for example ~/apps), dump databases to files on a schedule, and send the lot off the server with Borg to a storage box.