A Docker VPS is a general-purpose Linux server for running containerized web applications, databases, workers, bots, and development tools. Docker does not decide how much compute an application needs, secure a published port, or back up a volume. This guide installs the current stable Docker Engine packages, tests Compose, and establishes safe defaults before real application data is added.
The commands target Ubuntu 24.04 LTS on x86-64 and Docker's official stable apt repository. Do not use Docker's convenience install script as an opaque production bootstrap, and do not install Docker Desktop on a headless VPS.
Docker VPS requirements and LayerOne cost
| Item | Practical starting point |
|---|---|
| Minimum RAM | 1 GB for the engine and one genuinely small service; 512 MB is usually too constrained once image pulls and package updates overlap |
| Recommended RAM | 2 GB for Docker, the OS, and one or two light containers with update headroom |
| Expected CPU usage | The idle daemon is commonly below 1–3% of one vCPU; image builds, compression, databases, and application traffic can saturate every allocated vCPU |
| Storage requirement | Reserve at least 10 GB for Ubuntu, package caches, images, writable layers, logs, and update overlap; 40 GB is a more useful starting disk |
| Exact LayerOne SKU | gc.micro (layerone-starter): 1 vCPU, 2 GB RAM, 40 GB disk |
| Expected monthly cost | $0.0068/hour, about $5.00 for 730 hours at the catalog's monthly equivalent |
These are host baselines, not an application sizing promise. Add the documented RAM, CPU, and data growth of every container, then leave headroom for pulls and upgrades. A database, media server, CI builder, or multi-tenant platform can require a much larger plan.
LayerOne meters the server hourly from prepaid credit. The monthly equivalent excludes taxes, optional services, third-party software, and transfer beyond the account allowance. Check the current VPS pricing before deployment.
1. Prepare and update Ubuntu
Complete Your first hour on a new server, then apply all operating-system updates:
sudo apt update
sudo apt full-upgrade -y
[ -f /var/run/reboot-required ] && sudo reboot
Reconnect after any reboot and confirm the release:
. /etc/os-release
printf '%s %s
' "$NAME" "$VERSION_ID"
uname -m
2. Add Docker's signed stable repository
Install the signing-key prerequisites and save Docker's key in apt's keyring directory:
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
Create the deb822 repository definition. The codename and architecture are read from this server rather than frozen into the guide:
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<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
apt-cache policy docker-ce
Review that the candidate comes from download.docker.com, then install the
engine, container runtime, Buildx, and Compose plugin:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
3. Verify Engine and Compose
Run Docker's official smoke container and inspect the installed versions:
sudo docker run --rm hello-world
sudo docker version
sudo docker compose version
sudo docker info --format '{{.ServerVersion}} {{.CgroupDriver}}'
This guide keeps sudo on administrative Docker commands. Adding a person to
the docker group gives that person a practical path to root on the host; do it
only for trusted administrators who already have equivalent authority.
4. Set firewall rules before publishing containers
Start from inbound deny and replace the example administrator CIDR:
ADMIN_CIDR="198.51.100.24/32"
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from "$ADMIN_CIDR" to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose
Mirror that rule in the LayerOne cloud firewall: inbound DROP, outbound
ACCEPT, and TCP 22 only from the administrator CIDR. Add TCP 80 and 443 from
0.0.0.0/0 only when this host intentionally serves public HTTP and HTTPS.
Never open a database, Redis, Docker API, metrics, or admin-panel port just to
make a local container work.
Docker-published ports can bypass UFW
Docker documents that ports published on all interfaces can bypass rules managed by UFW or firewalld. The LayerOne cloud firewall is upstream of the guest and remains the reliable outer boundary. Still bind private services to loopback or publish no port at all.
Use this exposure model:
| Container port use | Compose configuration | Cloud firewall |
|---|---|---|
| Another container needs it | No ports entry; use the Compose service name |
No rule |
| Host reverse proxy needs it | 127.0.0.1:3000:3000 |
No rule for 3000 |
| Public web service | Prefer Caddy/Nginx on 80/443 and a loopback backend | Allow 80/443 only |
| Database or cache | No public publish; internal Docker network or private interface | No public rule |
5. Test a loopback-only Compose service
This disposable example pins the Nginx 1.28 Alpine line and makes it reachable only from the VPS itself:
sudo install -d -m 0750 /opt/docker-smoke
sudo tee /opt/docker-smoke/compose.yaml >/dev/null <<'YAML'
services:
web:
image: nginx:1.28-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
YAML
cd /opt/docker-smoke
sudo docker compose config
sudo docker compose up -d
curl -I http://127.0.0.1:8080/
sudo docker compose ps
A remote connection to SERVER_IP:8080 must fail. Remove only this disposable
test stack when finished:
cd /opt/docker-smoke
sudo docker compose down
For a real web application, keep its port on loopback and publish it through a reverse proxy with HTTPS.
6. Cap container log growth
Docker's default json-file logs can consume the root filesystem indefinitely.
On a new host with no existing daemon configuration, create a rotation policy:
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
JSON
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl restart docker
sudo docker info --format '{{.LoggingDriver}}'
If /etc/docker/daemon.json already exists, merge the keys instead of
overwriting unrelated runtime settings. A service can also set its own Compose
logging limits. The new daemon default applies only to containers created
after the restart, so recreate any existing service that must adopt it.
7. Monitor memory and disk instead of guessing
Inspect running containers and Docker's disk accounting:
sudo docker stats --no-stream
sudo docker system df -v
df -h / /var/lib/docker
free -h
Set per-service CPU and memory limits based on a measured workload so one container cannot consume the entire host. Keep alerts outside the VPS for low disk, OOM kills, container restarts, and application health. See Monitor your VPS.
Remove resources only after reading the candidate list. docker image prune
removes unused images; docker system prune --volumes can delete data you meant
to retain and should never be an unreviewed scheduled task.
8. Back up named volumes and databases correctly
A named Docker volume is still on this VPS. For a file-based service that has been stopped cleanly, archive a named volume to the current directory:
sudo docker run --rm --mount source=myapp_data,target=/data,readonly --mount type=bind,src="$PWD",dst=/backup alpine:3.23 tar -czf /backup/myapp-data.tar.gz -C /data .
Replace myapp_data with the actual volume name. A live PostgreSQL, MySQL, or
other transactional database needs its database-native dump or documented
backup process; copying changing files is not a consistent backup. Encrypt the
result, move it off the VPS, and perform a test restore. LayerOne does not take
backups of the guest disk; read Backups and snapshots.
9. Upgrade Docker and application images separately
Upgrade Engine packages from the stable repository:
sudo apt update
sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo docker version
Each application image has its own release notes and migrations. Back up first, then update one Compose project deliberately:
cd /opt/your-application
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100
Prefer a maintained stable major or major/minor tag when the upstream project publishes one. A digest gives exact reproducibility but also requires a planned process to receive security updates.
Security checklist
- Keep the LayerOne cloud firewall inbound policy at
DROP. - Publish databases and internal dashboards to no host port or to
127.0.0.1. - Never expose Docker's socket or unauthenticated TCP API to an application.
- Treat membership in the
dockergroup as root access. - Use official images, inspect Compose files, and avoid unbounded third-party tags.
- Do not bake passwords into images or commit
.envfiles; use restrictive secret files or a dedicated secret manager. - Run containers as non-root and read-only where the image supports it.
- Back up application data off-server and test restoration before upgrades.