Vultr Docker Setup 2026: Complete Guide to Containerize Your Applications

May 24, 2026 · Tutorial

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:

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:

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:

Next Steps

You've containerized and deployed a real application. Where to go from here:

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.

🔗 Recommended Platforms

BC.GAME | Cloudbet

🎯 Recommended Betting Platforms

BC.GAME - Up to 300% Bonus Cloudbet - Best Crypto Sportsbook