Disclosure: we may earn a commission or referral reward from CloudCone, Contabo, DigitalOcean (account credit, not cash), Hostwinds, InterServer, RackNerd, VPSDime and Vultr if you sign up through our links. It does not change the price you pay. Every other provider pays us nothing. Prices in this guide are as of the date shown and may have changed; see the comparison pages for the latest checked prices.
Guides · BLOG
How to install Docker on any Linux VPS in 2026
Some prices quoted in this guide are out of date. It is being rebuilt on our price index; for current prices use the comparison pages, which show when each price was checked.
Docker is the fastest way to deploy an app on a VPS in 2026. This guide walks through the correct installation on the four most common VPS distros (Ubuntu, Debian, CentOS, Rocky), followed by the essential first configuration.
For Ubuntu / Debian (10-minute install)
```bash
Remove any old Docker installations
apt remove docker docker-engine docker.io containerd runc 2>/dev/null || true
Install prerequisites
apt update
apt install ca-certificates curl gnupg lsb-release
Add Docker's official GPG key
mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Add the repository (Ubuntu-specific; for Debian, replace 'ubuntu' with 'debian')
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker and Compose plugin
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
Verify:
```bash
docker run hello-world
```
You should see the "Hello from Docker!" message.
For CentOS / Rocky / RHEL
```bash
Remove old versions
yum remove docker docker-client docker-client-latest docker-common docker-latest \
docker-latest-logrotate docker-logrotate docker-engine 2>/dev/null || true
Install Docker repo
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Install Docker
yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Start and enable
systemctl enable --now docker
```
Add a non-root user to the docker group
Running docker commands as root every time is dangerous. Add a non-root user:
```bash
usermod -aG docker deploy
```
Log out and back in for the group change to take effect. Now deploy can run docker without sudo.
Security note: any user in the docker group has effective root on the host (via docker run --privileged -v /:/host ...). Only add trusted users.
Configure Docker daemon
Create /etc/docker/daemon.json:
```json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 }
]
}
```
Restart Docker:
```bash
systemctl restart docker
```
This limits log rotation (default is unlimited — logs will eat your disk).
Install docker-compose (already covered by Compose plugin)
The Docker installation above includes the docker compose plugin (note: docker compose, not docker-compose).
Test:
```bash
docker compose version
```
Should return the version.
For the older standalone docker-compose command (if some tutorial requires it):
```bash
apt install docker-compose # Debian/Ubuntu
or
pip install docker-compose # cross-platform
```
Configure the firewall
Docker punches through UFW/iptables by default. This is by design but confusing. If you want Docker's exposed ports to respect UFW:
```bash
Add to /etc/ufw/after.rules at the bottom:
*filter
:DOCKER-USER - [0:0]
:ufw-user-input - [0:0]
-A DOCKER-USER -j ufw-user-input
-A DOCKER-USER -j RETURN
COMMIT
```
Restart UFW: ufw reload.
Now Docker's exposed ports go through UFW.
Essential first commands
```bash
List running containers
docker ps
List all containers (including stopped)
docker ps -a
List images
docker images
Remove unused stuff (frees a lot of disk)
docker system prune -af --volumes
Get a shell in a running container
docker exec -it CONTAINER_NAME bash
View container logs
docker logs -f CONTAINER_NAME
Check disk usage by Docker
docker system df
```
Disk cleanup cron
Docker eats disk over time (unused images, stale layers, stopped containers). Set a weekly cleanup:
```bash
crontab -e
Add:
0 4 0 docker system prune -af --volumes > /var/log/docker-prune.log 2>&1
```
Sunday 4am cleanup keeps your disk sane.
Deploy a first app
Create a docker-compose.yml:
```yaml
services:
web:
image: nginx:latest
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html:ro
```
```bash
mkdir html
echo "Hello Docker!" > html/index.html
docker compose up -d
```
Visit http://your-vps-ip/ — you should see "Hello Docker!".
Common gotchas
"Cannot connect to the Docker daemon"
- Docker daemon not running:
systemctl start docker. - Or you forgot to log out and back in after adding your user to the docker group.
"Bind for 0.0.0.0:80 failed: port is already allocated"
- Something else (Nginx, Apache) is using port 80. Stop it or use a different port.
Docker takes all my disk.
- Run
docker system prune -af --volumes. Set the weekly cron above.
"exec /bin/sh: exec format error"
- You're on ARM64 and pulled an x86_64 image (or vice versa). Use
docker pull --platform linux/amd64 imagefor the right arch.
What NOT to run in Docker on a small VPS
- A database as a "temporary" container. Bind mount /var/lib/mysql or /var/lib/postgres to a volume so data persists.
--privilegedcontainers you don't understand. Effective root on the host.- Random Docker images from Docker Hub without checking the source. Supply chain attacks are real.
Rootless Docker: worth it?
Docker's rootless mode runs the daemon as an unprivileged user. Better security posture at the cost of some feature limitations (port binding under 1024 requires setcap or a reverse proxy). For a single-tenant VPS running trusted workloads, rootless is nice-to-have not need-to-have. For a shared VPS or a workload running untrusted third-party code, rootless is worth the setup complexity. Full instructions at docs.docker.com/engine/security/rootless/.
Podman as an alternative
Red Hat's Podman is a drop-in replacement for Docker with a compatible CLI (alias docker=podman works). Rootless by default. Preferred on Fedora/RHEL/Rocky by some operators for the security posture. On Ubuntu/Debian, Docker is still the default choice and community expects it.
Memory considerations
Docker's daemon uses ~50-100 MB of RAM baseline. Each running container adds its own footprint. On a 1 GB VPS, the overhead is meaningful; on a 4 GB Hetzner CX22 it's noise. If your workload is small (a Discord bot, a single small web service), running natively without Docker saves you the daemon overhead.
Image size discipline
Docker images grow. A naive Node.js image can be 1 GB; a well-optimised one is 100 MB. Multi-stage builds and Alpine/distroless base images reduce size dramatically:
```dockerfile
Bad: 1 GB
FROM node:20
COPY . .
RUN npm install
CMD ["node", "index.js"]
Better: 200 MB
FROM node:20-alpine
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "index.js"]
Best: 100 MB with multi-stage
FROM node:20-alpine as builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
COPY --from=builder /dist ./dist
COPY --from=builder /node_modules ./node_modules
CMD ["node", "dist/index.js"]
```
Smaller images pull faster, use less disk, and deploy faster on redeploys.
Related guides
- Best hosting for a Docker / self-hosted stack — full self-hosted stack patterns.
- Best hosting for a Node.js API — Docker vs native deployment.
- How to set up automated backups on any VPS — backup Docker volumes.