Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to Set Up a Docker VPS on Ubuntu

Build a secure Docker VPS with official Ubuntu packages, Compose, two firewalls, private container ports, log limits, backups, and upgrades.

7 min read

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 docker group as root access.
  • Use official images, inspect Compose files, and avoid unbounded third-party tags.
  • Do not bake passwords into images or commit .env files; 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.

Official references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.