How to Use Docker for Small Server Deployments

by David Park
How to Use Docker for Small Server Deployments

This guide walks you through a production-ready Docker setup on a single Ubuntu 24.04 VPS for small server deployments. By the end, you'll have Docker installed and locked to a stable version, a non-root deploy user, a running Nginx container with restart policy, and log rotation configured so your $5 Hetzner box doesn't fill its disk overnight.

Prerequisites

  • A fresh Ubuntu 24.04 VPS (1 vCPU / 2 GB RAM minimum — a Hetzner CX22 at ~$4.50/month works fine)
  • A non-root user with sudo privileges
  • SSH access to the server
  • Basic familiarity with systemd and the Linux command line
  • Ports 80 and 443 open in your firewall or cloud security group

Step 1 — Remove Conflicting Packages

Ubuntu 24.04 ships with stub Docker packages that conflict with the official release. Remove them first.

sudo apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc

Expected output ends with 0 upgraded, 0 newly installed, N removed.

Step 2 — Add the Official Docker APT Repository

Install the dependencies needed to fetch Docker's signed repository.

sudo apt-get update
sudo apt-get install -y ca-certificates curl

Create the keyring directory and download Docker's GPG key.

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

Add the repository to APT sources.

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

Update the package index so APT sees the new source.

sudo apt-get update

Step 3 — Install Docker Engine at a Pinned Version

Pinning a version prevents an unattended upgrade from breaking your deployment mid-week. List available versions first.

apt-cache madison docker-ce | head -5

Sample output:

 docker-ce | 5:27.3.1-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages
 docker-ce | 5:27.2.1-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages

Install the latest stable version shown (substitute the version string if yours differs).

VERSION_STRING="5:27.3.1-1~ubuntu.24.04~noble"
sudo apt-get install -y \
  docker-ce=$VERSION_STRING \
  docker-ce-cli=$VERSION_STRING \
  containerd.io \
  docker-buildx-plugin \
  docker-compose-plugin

Pin the packages so apt upgrade won't touch them.

sudo apt-mark hold docker-ce docker-ce-cli containerd.io

Step 4 — Configure the Docker Daemon for Small Servers

The default Docker daemon settings are tuned for machines with large disks and abundant RAM. Override them for a small VPS.

Create or edit /etc/docker/daemon.json.

sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "live-restore": true
}
EOF

What each option does:

  • log-driver + log-opts — caps container logs at 30 MB total (3 × 10 MB) per container, preventing disk exhaustion.
  • storage-driver: overlay2 — the correct driver for ext4 and xfs on modern kernels; avoids the deprecated devicemapper.
  • live-restore: true — containers keep running if the Docker daemon restarts (e.g., after a daemon config change), giving you zero-downtime daemon upgrades.

Reload the daemon to apply the config.

sudo systemctl reload docker

Step 5 — Create a Non-Root Deploy User

Adding your deploy user to the docker group lets it run containers without sudo. Never add your personal admin account to this group — docker group membership is effectively root.

Create a dedicated deploy user.

sudo useradd -m -s /bin/bash deploy

Add it to the docker group.

sudo usermod -aG docker deploy

Switch to the deploy user to confirm group membership takes effect.

sudo -u deploy docker info | grep -i "server version"

Expected output:

 Server Version: 27.3.1

Step 6 — Deploy a Container with a Restart Policy

This step runs Nginx as a real-world example. The same pattern applies to any image you deploy.

Create a directory for your app's data and config under the deploy user's home.

sudo mkdir -p /home/deploy/apps/nginx/html
echo '<h1>Running on Docker</h1>' | sudo tee /home/deploy/apps/nginx/html/index.html
sudo chown -R deploy:deploy /home/deploy/apps

Start the container as the deploy user with --restart unless-stopped so it survives reboots and daemon restarts.

sudo -u deploy docker run -d \
  --name nginx-app \
  --restart unless-stopped \
  -p 80:80 \
  -v /home/deploy/apps/nginx/html:/usr/share/nginx/html:ro \
  nginx:1.27-alpine

Flags explained:

  • -d — detached mode; container runs in the background.
  • --restart unless-stopped — restarts automatically unless you explicitly run docker stop.
  • -v … :ro — mounts your HTML directory read-only inside the container; the container process cannot modify it.
  • nginx:1.27-alpine — pinned minor version on the Alpine base image; roughly 45 MB vs 190 MB for the Debian variant.

Step 7 — Configure Log Rotation at the OS Level

The daemon-level log cap from Step 4 handles container log files. For the Docker daemon's own log (written by journald), cap it separately.

Edit /etc/systemd/journald.conf.

sudo tee -a /etc/systemd/journald.conf > /dev/null <<'EOF'

# Docker daemon log limits
SystemMaxUse=200M
SystemKeepFree=100M
EOF

Restart journald to apply.

sudo systemctl restart systemd-journald

Verify It Works

Run each check in order. All should pass before you call the setup complete.

1. Docker daemon is active.

sudo systemctl is-active docker
active

2. Container is running with the correct restart policy.

sudo -u deploy docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES       STATUS          PORTS
nginx-app   Up 2 minutes    0.0.0.0:80->80/tcp

3. HTTP response from the container.

curl -s http://localhost | grep -o 'Running on Docker'
Running on Docker

4. Log rotation config is applied.

sudo -u deploy docker inspect nginx-app \
  --format '{{.HostConfig.LogConfig.Type}} max-size={{index .HostConfig.LogConfig.Config "max-size"}}'
json-file max-size=10m

5. Container survives a daemon restart.

sudo systemctl restart docker
sleep 5
sudo -u deploy docker ps --filter name=nginx-app --format "{{.Status}}"
Up 4 seconds

Troubleshooting

docker: command not found after installation The APT repository was not added correctly. Re-run Step 2 and confirm /etc/apt/sources.list.d/docker.list exists, then re-run Step 3.

permission denied when the deploy user runs docker The group change requires a new login session. Run sudo -u deploy docker info or log out and back in as deploy.

Port 80 already in use (Bind for 0.0.0.0:80 failed) Identify the occupying process with sudo ss -tlnp | grep ':80'. Stop the conflicting service (commonly apache2 or a host-level nginx) with sudo systemctl stop apache2.

Container exits immediately Check the container logs: sudo -u deploy docker logs nginx-app. An image misconfiguration or missing mount path is the usual cause.

Disk usage growing despite log caps Untagged images and stopped containers accumulate. Schedule a weekly prune: sudo -u deploy docker system prune -f. Add it to cron with crontab -e -u deploy and the line 0 3 * * 0 /usr/bin/docker system prune -f.

live-restore warning in daemon logs Some older kernel versions log a warning about live-restore and user namespaces together. Check with sudo journalctl -u docker | grep -i warn. If it's cosmetic and containers run fine, ignore it; otherwise remove live-restore from /etc/docker/daemon.json and reload.


Next Steps

With Docker for small server deployments running and hardened, the logical next steps are:

  • Add Docker Composedocker compose (the plugin installed in Step 3) lets you define multi-container stacks in a single compose.yml file. Commit that file to a private Git repo and your deploys become repeatable.
  • Set up a reverse proxy — Run Caddy or Nginx Proxy Manager as a container to terminate TLS and route traffic to multiple apps on the same VPS without exposing additional ports.
  • Automate image updatesWatchtower (run as a container) polls Docker Hub and restarts containers when a new image tag appears. Combine it with pinned patch versions to stay current without surprise breakage.
  • Add resource limits — For multi-tenant or multi-app VPS setups, add --memory 256m --cpus 0.5 to your docker run commands (or the equivalent in compose.yml) to prevent one misbehaving container from starving the others.