Containers changed how we deploy software. No more "works on my machine" — your app, its dependencies, and its runtime ship together as a sealed unit that runs identically on your laptop, a colleague's machine, and a production server. Docker made this accessible. Vultr makes it cheap. Together, they're one of the fastest paths from code to running service.
This guide covers installing Docker on a Vultr VPS, configuring it for production use, setting up Docker Compose for multi-container projects, hardening the setup, and deploying a real web application. Everything works on a $5/month instance — no GPU required.
Why Docker on Vultr?
Traditional server management means fighting dependency conflicts. Your Node app needs v18, your Python script needs v3.11, your Ruby app needs 3.0. On a bare server, someone wins and everyone else适配 (adapts). Docker isolates each application in its own container with its own dependencies, so nothing conflicts.
Vultr's VPS instances are ideal for Docker because:
- Full root access — Install whatever you need, modify kernel parameters, mount filesystems
- NVMe storage options — Container image pulls are fast; registry operations don't bottleneck on disk I/O
- Hourly billing — Spin up a Docker host for a project, destroy it when done, pay only for what you used
- Global regions — Deploy containers close to your users; keep data in specific jurisdictions if needed
Prerequisites
You'll need:
- A Vultr VPS with Ubuntu 24.04 LTS (recommended) or Debian 12
- At least 1GB RAM — Docker Engine itself needs ~200MB; containers add on top of that
- Non-root user with sudo privileges (follow the Vultr Ubuntu setup guide if you haven't set this up yet)
Step 1: Install Docker Engine on Vultr
The fastest path is using Docker's convenience script. For production setups, the official APT repository gives you version control. Here's both:
Quick Install (Convenience Script)
# SSH into your Vultr VPS as your sudo user
ssh user@your_vultr_ip
# Download and run Docker's convenience script
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh
# Executing docker install script, branch: main
# Installation complete!
Production Install (Official APT Repository)
# Update package index and install prerequisites
sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg lsb-release
# Add Docker's official 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
# Set up the 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 > /dev/null
# Install Docker Engine
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
The convenience script is fine for dev machines. The APT repository method gives you control over which Docker version you're running — critical for production stability.
Step 2: Configure Docker for Production Use
Manage Docker as a Non-Root User
By default, Docker requires root privileges. Add your user to the docker group to avoid sudoing every container command:
# Add your user to the docker group
sudo usermod -aG docker $USER
# Apply the new group membership (log out and back in, or run):
newgrp docker
# Verify it works — you should see your username and groups
id | grep docker
uid=1000(username) gid=1000(username) groups=1000(username),4(adm),24(cdrom),27(sudo),999(docker)
Configure Docker Daemon Options
For a production VPS, configure Docker's daemon with sensible defaults. Create or edit the daemon configuration file:
sudo nano /etc/docker/daemon.json
Add this configuration:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 }
],
"live-restore": true,
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 65536
}
}
}
This sets up structured logging with rotation (max 10MB per file, 3 files max), uses the overlay2 storage driver (fast and stable on Ubuntu), enables live restore so containers survive Docker restarts, and raises file descriptor limits for high-concurrency workloads.
# Restart Docker to apply changes
sudo systemctl restart docker && sudo systemctl enable docker
# Verify Docker is running
docker info | head -20
Step 3: Deploy a Web Application with Docker Compose
Docker Compose manages multi-container applications. Let's deploy a real Python Flask application with a Redis cache layer — a common production pattern.
Project Structure
# Create project directory
mkdir -p ~/flask-app && cd ~/flask-app
# Create app directory structure
mkdir -p app && mkdir -p nginx
Create the Flask Application
cat > app/app.py << 'EOF'
from flask import Flask, jsonify
from redis import Redis
import os
app = Flask(__name__)
redis_host = os.environ.get('REDIS_HOST', 'redis')
redis = Redis(host=redis_host, port=6379, decode_responses=True)
@app.route('/')
def home():
return jsonify({
'message': 'Docker on Vultr is running smoothly',
'hits': redis.incr('page_hits'),
'version': '1.0'
})
@app.route('/health')
def health():
return jsonify({'status': 'healthy'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF
cat > app/requirements.txt << 'EOF'
flask==3.0.*
redis==5.0.*
gunicorn==21.2.*
EOF
Create the Dockerfile
cat > app/Dockerfile << 'EOF'
FROM python:3.12-slim
WORKDIR /app
# Install dependencies before copying source
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application source
COPY . .
# Use gunicorn for production
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
EOF
Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
web:
build: ./app
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
- FLASK_ENV=production
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped
networks:
- app-network
volumes:
redis-data:
networks:
app-network:
driver: bridge
EOF
Build and Deploy
# Build and start the application
docker compose up -d --build
# Building web ... done
# Containerizing... done
# Starting flask-app-web-1 ... done
# Starting flask-app-redis-1 ... done
# Check running containers
docker compose ps
NAME IMAGE STATUS PORTS
web-1 app-web running 0.0.0.0:5000->5000/tcp
redis-1 redis:7 running 0.0.0.0:6379->6379/tcp
# Test the application
curl http://localhost:5000/
{"message":"Docker on Vultr is running smoothly","hits":1,"version":"1.0"}
# Refresh to verify Redis is tracking hits
curl http://localhost:5000/
{"message":"Docker on Vultr is running smoothly","hits":2,"version":"1.0"}
Step 4: Secure Your Docker Setup
Docker containers run with isolated namespaces by default, but a misconfigured deployment can expose your host. Here's how to harden:
1. Enable Docker Content Trust
Prevents pulling unsigned images:
export DOCKER_CONTENT_TRUST=1
# Add to ~/.bashrc to make it permanent
echo 'export DOCKER_CONTENT_TRUST=1' >> ~/.bashrc
2. Limit Container Capabilities
Don't give containers more Linux capabilities than necessary:
# Run containers without NET_ADMIN capability (prevent network changes)
docker run --cap-drop=NET_ADMIN myapp:latest
# Run as non-root user inside the container
docker run -u 1000 myapp:latest
3. Scan Images for Vulnerabilities
# Install Trivy (open source vulnerability scanner)
sudo apt-get install -y trivy
# Scan your Flask image
trivy image flask-app-web:latest
2026-05-26 10:00:00 Scanning flask-app-web:latest...
Total: 3 (UNKNOWN: 0, LOW: 2, MEDIUM: 1, HIGH: 0, CRITICAL: 0)
LOW: CVE-2026-1 — python:3.12-slim base image, upgrade to 3.12.1
MEDIUM: CVE-2026-2 — flask: authentication bypass in < 3.0.1
4. Use a Firewall to Restrict Container Traffic
# Install UFW if not already present
sudo apt install -y ufw
# Allow SSH and web traffic only; block container-to-container exposure
sudo ufw default deny incoming && sudo ufw allow 22/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable
# Verify UFW status
sudo ufw status verbose
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
Step 5: Set Up Container Monitoring
Running containers is one thing; knowing when one is misbehaving is another. Here's a lightweight monitoring setup:
# Check container resource usage
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
NAME CPU % MEM USAGE
web-1 0.23% 72.5MiB / 1GiB
redis-1 0.11% 12.3MiB / 1GiB
# View container logs (follow mode)
docker compose logs -f web
# Check disk usage of Docker
docker system df
TYPE TOTAL ACTIVE SIZE
Images 2 2 142MB
Containers 2 2 0B
Local Volumes 1 1 0B
Step 6: Automate Cleanup
Containers accumulate. Old images, stopped containers, unused volumes — they eat disk space. Set up a cleanup routine:
# Clean unused images, stopped containers, and build cache
docker system prune -f
Total reclaimed space: 245MB
# Add this to a cron job for automatic cleanup
sudo crontab -e
Add this line to run weekly cleanup at 3 AM every Sunday:
0 3 * * 0 /usr/bin/docker system prune -f >> /var/log/docker-cleanup.log 2>&1
Deploy from GitHub Actions
Once your Vultr VPS is Docker-ready, you can automate deployments from GitHub. Here's the workflow:
cat > .github/workflows/deploy.yml << 'EOF'
name: Deploy to Vultr
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./app
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/flask-app:latest
- name: Deploy on Vultr
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VULTR_HOST }}
username: ${{ secrets.VULTR_USER }}
key: ${{ secrets.VULTR_SSH_KEY }}
script: |
cd ~/flask-app
docker compose pull
docker compose up -d
EOF
Set your Vultr server IP, username, and SSH private key as GitHub secrets, and every push to main automatically deploys your latest image to your Vultr VPS.
Conclusion
Docker on Vultr is a production-grade combination: containers give you consistency and isolation, while Vultr's bare VPS gives you control and cost efficiency. The setup covered here — Docker Engine with proper daemon config, Docker Compose for multi-container apps, security hardening with capability limits and UFW, and automated cleanup — is production-ready out of the box.
Whether you're running a Flask app like this one, a Node.js API, a Python data pipeline, or a full microservices stack, Docker on Vultr handles it. Start with a $5/month instance, scale vertically when you need more resources, and deploy in minutes not hours.
Deploy a Docker-ready VPS on Vultr with $250 free credit — no annual commitment, cancel anytime.