Containers changed how we deploy software. No more "works on my machine" — your app ships with its dependencies, runs identically in dev and production, and scales horizontally without reinstalling things on each new server. Docker on Vultr is one of the fastest paths from code to running service.
This guide covers the full stack: installing Docker on Ubuntu 24.04, configuring Docker Compose for multi-container apps, securing the daemon, setting up a private registry, and deploying a real production workload — a Node.js API with PostgreSQL and Nginx reverse proxy.
Why Docker on Vultr?
Vultr's $6/month instance (1 vCPU, 1GB RAM, 32GB NVMe) is enough to run Docker and host 3-5 containers for development, staging, or low-traffic production services. When your traffic grows, vertically scale to a 4 vCPU / 8GB plan without changing your Docker configuration — containers abstract the hardware.
Key advantages:
- NVMe storage — Container image pulls are fast, even large images (500MB+) download in seconds
- Hourly billing — Spin up a Docker host for a quick test, pay cents for minutes used
- Full root access — No managed container services that restrict what you can run
- 25+ global locations — Deploy your registry mirror or build nodes closer to your team
Prerequisites
This guide assumes you have a running Vultr Ubuntu 24.04 instance. If you haven't set one up yet, follow the Vultr Ubuntu Setup guide first — it walks through server creation, initial SSH connection, and security hardening. Come back here once you have a clean Ubuntu server accessible via SSH.
What you'll need:
- Vultr instance (Ubuntu 24.04 LTS)
- SSH access with sudo privileges
- Domain name (optional, for production deployments)
Installing Docker on Ubuntu 24.04
Ubuntu 24.04 ships with an older Docker package in its default repositories. For 2026, you'll want Docker Engine 27+ with BuildKit and containerd as the runtime. The official Docker repository gives you the latest stable release.
Add Docker's Official Repository
# Update apt index and install prerequisites
sudo apt update && sudo apt install -y ca-certificates curl gnupg lsb-release
# Add Docker's GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Add Docker stable repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
# Install Docker Engine and related packages
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify the Installation
docker --version
Docker version 27.4.1, build b9f83d2
docker compose version
Docker Compose version v2.35.1
sudo docker run --rm hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Run Docker Without Sudo (Recommended)
By default, Docker requires sudo. Add your user to the docker group to run containers without privilege escalation:
sudo usermod -aG docker $USER
# Log out and back in, or run:
newgrp docker
# Verify (run without sudo):
docker ps
Configuring Docker for Production
Default Docker settings are fine for local development but need tuning for production. Here's what to configure on a Vultr instance.
Set Up the Docker Daemon Metrics
Enable the Docker metrics endpoint so Prometheus or similar can monitor your containers:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json << 'EOF'
{
"metrics-addr": "0.0.0.0:9323",
"registry-mirrors": [],
"live-restore": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
sudo systemctl restart docker
The live-restore option keeps containers running when the Docker daemon restarts — essential for production uptime. max-size and max-file prevent log files from filling your NVMe disk.
Configure Docker to Start on Boot
sudo systemctl enable docker
Created symlink /etc/systemd/system/sysinit.target.wants/docker.service → /lib/systemd/system/docker.service.
Building a Real Application: Node.js API + PostgreSQL + Nginx
Let's put Docker to work with a realistic multi-container stack. We'll deploy a Node.js REST API backed by PostgreSQL, with Nginx as a reverse proxy handling SSL termination.
Project Structure
mkdir -p ~/docker-stack && cd ~/docker-stack
mkdir -p api/src nginx letsencrypt/data
touch docker-compose.yml api/Dockerfile api/src/index.js api/package.json nginx/nginx.conf
docker-stack/
├── docker-compose.yml
├── api/
│ ├── Dockerfile
│ ├── package.json
│ └── src/index.js
└── nginx/
└── nginx.conf
The Node.js API
Create a simple Express API:
cat > api/package.json << 'EOF'
{
"name": "api",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"express": "^4.21.0",
"pg": "^8.13.0"
}
}
EOF
cat > api/src/index.js << 'EOF'
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({
host: process.env.POSTGRES_HOST,
port: 5432,
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
});
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/data', async (req, res) => {
try {
const result = await pool.query('SELECT NOW() as now');
res.json({ query_time: result.rows[0].now, server: 'node-api' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API listening on port ${PORT}`);
});
EOF
cat > api/Dockerfile << 'EOF'
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/
EXPOSE 3000
CMD ["npm", "start"]
EOF
Nginx Configuration
Nginx routes external traffic to the API container and provides SSL termination:
cat > nginx/nginx.conf << 'EOF'
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://api:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
EOF
Docker Compose File
The compose file ties everything together:
cat > docker-compose.yml << 'EOF'
version: '3.9'
services:
api:
build: ./api
restart: always
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: changeme_strong_password
PORT: 3000
depends_on:
postgres:
condition: service_healthy
networks:
- app-net
postgres:
image: postgres:17-alpine
restart: always
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: changeme_strong_password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-net
nginx:
image: nginx:1.27-alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./letsencrypt/data:/etc/letsencrypt:ro
depends_on:
- api
networks:
- app-net
volumes:
pgdata:
driver: local
networks:
app-net:
driver: bridge
EOF
Launch the Stack
# Build images and start all containers
docker compose up -d --build
[+] Building api... 10.2s
[+] Running 3/3 2.6s
✔ Network app-net created
✔ Container docker-stack-postgres-1 Created
✔ Container docker-stack-api-1 Created
✔ Container docker-stack-nginx-1 Created
# Check status
docker compose ps
NAME IMAGE STATUS PORTS
api api Up (healthy) 3000/tcp
postgres postgres:17 Up (healthy) 5432/tcp
nginx nginx:1.27 Up 0.0.0.0:80->80/tcp, :::80->80/tcp
# Test the API through Nginx
curl http://localhost/health
{"status":"ok","timestamp":"2026-05-24T10:00:01.234Z"}
curl http://localhost/api/data
{"query_time":"2026-05-24T10:00:05.000Z","server":"node-api"}
Managing Running Containers
Once your stack is running, day-to-day operations:
# View logs (all services)
docker compose logs -f
# View logs for a specific service
docker compose logs -f api
# Restart a specific service
docker compose restart api
# Update and redeploy (e.g., after code changes)
docker compose up -d --build api
# Check resource usage
docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM %
api docker-api-1 0.12% 78.5MiB / 987MiB 7.95%
postgres docker-post-1 0.06% 89.2MiB / 987MiB 9.04%
nginx docker-nginx-1 0.01% 7.8MiB / 987MiB 0.79%
# Stop and remove containers (preserves volumes)
docker compose down
# Stop and remove containers AND volumes (destroys data!)
docker compose down -v
Adding SSL with Let's Encrypt
For production, you need HTTPS. Certbot with Nginx is the standard approach:
# Install certbot
sudo apt install -y certbot python3-certbot-nginx
# Obtain certificate (replace with your domain)
sudo certbot --nginx -d your-domain.com --noninteractive --agree-tos -m your@email.com
# Certbot auto-renews certificates, but test the renewal
sudo certbot renew --dry-run
For a fully containerized SSL setup (without certbot on the host), use traefik or Caddy as your reverse proxy — both have built-in automatic HTTPS.
Docker on Vultr: Performance Considerations
A few things to keep in mind when running containers on Vultr's hardware:
- Memory limits — Without limits, a runaway container can exhaust RAM. Use
memory:in docker-compose.yml to cap each service - NVMe is fast but finite — Monitor
df -hand set log rotation as shown above. Docker images can fill a 32GB disk fast - CPU scheduling — Vultr's AMD EPYC CPUs handle container workloads well. Set
cpus: 0.5limits if you need per-service CPU caps - Network throughput — Docker's bridge networking saturates Vultr's 4Gbps link without issue
Next Steps
You've containerized and deployed a real application. Where to go from here:
- GitHub Actions CI/CD — Build and push Docker images on code changes, auto-deploy to Vultr
- Docker Swarm or Kubernetes — Scale to multi-host orchestration as your services grow
- Monitoring with Prometheus + Grafana — Container metrics, API latency dashboards, alerting
- Private Docker Registry — Host your own registry on Vultr's block storage for faster image pulls across instances
For scaling strategies and multi-instance deployments, see our Vultr scaling guide.
Conclusion
Docker on Vultr is a production-grade combination. You get the portability and reproducibility of containers with the performance and cost-efficiency of Vultr's NVMe-backed instances. The setup above handles a real-world Node.js + PostgreSQL + Nginx stack — which covers 70% of web applications.
Start with a $6/month instance for development and staging. When you're ready for production traffic, scale vertically to a 4 vCPU / 8GB plan — your docker-compose.yml works unchanged.
Ready to containerize your next project? Deploy your Docker host on Vultr with $250 free credit — no credit card required to start.