Docker on a small VPS — what to expect, what to avoid
Docker (and its rootless cousin Podman) is a popular way to run multiple applications on a single VPS in isolated containers. On a small VPS — say 1 vCPU and 2 GB RAM — Docker is workable but has tradeoffs that aren't obvious until you hit them. This article covers what realistically fits, what doesn't, and the operational patterns that make Docker on a small VPS pleasant rather than painful.
The honest baseline
Each Docker container is essentially a process group with isolated namespaces and a virtual filesystem layer. The container itself adds minimal overhead — kernel features doing the work, not a hypervisor or VM. But every container is also a real process tree that has to fit in your VPS's RAM and CPU.
Rough memory estimates per container (active, not just running):
| Container | Idle RAM | Under light load |
|---|---|---|
| nginx (static site) | 15 MB | 30 MB |
| PostgreSQL 15 (small DB) | 40 MB | 200-500 MB |
| Redis | 10 MB | 20-100 MB |
| Node app (Express) | 40 MB | 100-300 MB |
| Python app (Django + gunicorn) | 80 MB | 200-500 MB |
| Java app (Spring Boot) | 300 MB | 500-1500 MB |
| Nextcloud (PHP-FPM + Apache) | 200 MB | 400-800 MB |
| WordPress (PHP-FPM + nginx) | 120 MB | 250-500 MB |
On a 2 GB VPS, you can comfortably run 3-5 small services. On a 1 GB VPS, 1-2 small services. Java applications break this rule — budget heavily.
Install Docker (or Podman)
Debian / Ubuntu:
# Docker's official repo (current, supported):
apt update
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
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/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| tee /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
AlmaLinux: Podman is preinstalled and is the recommended container engine (rootless, daemonless, CLI-compatible with Docker). Use it directly:
podman run hello-world
Most of this article applies to both — substitute podman for docker mentally.
The four traps
1. The disk fills up silently
Docker accumulates layers, stopped containers, dangling images, and volume data. On a small VPS this can fill 20 GB faster than you'd expect.
# See what's using space:
docker system df
# Clean up aggressively:
docker system prune -a --volumes
Read the warning — --volumes deletes volumes not attached to a running container, which includes your database data if the database container is stopped at the moment.
Set up a weekly cleanup cron once you're comfortable with what you want to keep:
# /etc/cron.weekly/docker-prune
#!/bin/sh
docker image prune -a -f --filter "until=168h"
2. Memory pressure kills containers without obvious cause
When the VPS hits memory pressure, the kernel's OOM-killer picks something to kill. Docker containers usually score high (large process trees, no init protecting children) so they get reaped first.
Symptoms: a container that was running yesterday is now in "Exited (137)" state. Exit code 137 = killed by OOM.
Fixes:
- Limit each container's memory with
--memory:
Forces the container to OOM internally rather than triggering host-wide pressure.docker run --memory=256m --memory-swap=256m myimage - Add swap on the host. A small swap file (1-2 GB) absorbs short-term spikes. Performance is degraded when you're actually swapping, but it beats OOM-kills.
- Bigger VPS. If you're consistently OOM-killing, you're undersized for the workload.
3. Bridge networking and the firewall
Docker creates iptables/nftables rules to handle bridge networking and port publishing. These can conflict with UFW or firewalld rules — Docker's rules are added before yours, so a -p 80:80 publishes port 80 to the world even if UFW says block it.
Two patterns:
- Bind to localhost only when you don't want public access:
Then reverse-proxy through nginx/Caddy on the host.docker run -p 127.0.0.1:8080:80 myimage - Disable Docker's iptables rules entirelyand manage them yourself (advanced, only if you really understand iptables):
# /etc/docker/daemon.json { "iptables": false }
4. The image-pull bandwidth is real
Pulling a Node or Python base image is 100-300 MB; an Ubuntu image is 70 MB. If your VPS plan includes generous bandwidth this doesn't matter, but if you're rebuilding images frequently (CI, deploy-on-push) the egress to Docker Hub can add up.
Use multi-stage builds to keep your final images small (only the runtime, not the build dependencies) and cache base images locally with docker build --pull=false when you know the base hasn't changed.
Recommended pattern for a small-VPS deployment
For most "run a few services" use cases, this works well:
- Caddy or nginx on the host (not in a container) — handles TLS, reverse-proxies to containers on localhost-bound ports.
- Each application in its own container, bound to
127.0.0.1:PORT(not0.0.0.0). - Postgres / Redis on the host — databases are stateful and harder to manage in containers on small VPS where memory matters.
apt install postgresqland use the host instance. - docker compose for grouping related services with shared networks/volumes. Avoid running multiple compose stacks on a single small VPS; one is plenty.
- Volumes for state, with mountpoints in a known location like
/data/<service>so off-host backups can sweep one directory.
Alternatives to Docker for small VPS
- systemd services — for single Go/Rust/Java binaries, you don't need a container; just a systemd unit file. Lower overhead, fewer moving parts.
- Podman with --quadlet — systemd-managed containers; nice if you like Podman's rootless model.
- Nix / nix-darwin — declarative system config without containers. Steeper learning curve, powerful once you're in.
Docker isn't the only answer, and on a small VPS it's not always the best one. But when you need isolation between services, a documented runtime environment, or the ability to docker pull something you didn't write — it's hard to beat for ease.
Also Read
Powered by WHMCompleteSolution