How to Run Docker and Docker Compose on a VPS: Complete Production Setup Guide

How to Run Docker and Docker Compose on a VPS: Complete Production Setup Guide

Docker has become the standard way to package and deploy applications — providing consistent, reproducible environments that work identically from development laptop to production server. A KVM VPS is an ideal Docker host: it provides full kernel access (unlike OpenVZ containers), dedicated resources, and complete freedom to install and configure the Docker runtime. This guide walks through a complete production Docker setup on a Linux VPS, from installation through a multi-service deployment with Nginx reverse proxy, SSL, and persistent data management.

Prerequisites

  • A KVM-based VPS running Ubuntu 22.04 LTS (the examples below use Ubuntu; adapt package commands for Debian or AlmaLinux)
  • A non-root user with sudo privileges
  • SSH key authentication configured
  • UFW firewall enabled (see our VPS security hardening guide)

Note: Docker requires a real Linux kernel with namespace and cgroups support. It runs natively on KVM VPS instances but is not supported on OpenVZ-based VPS. Always verify your VPS uses KVM virtualization before attempting Docker installation.

Step 1: Install Docker Engine

Do not install Docker from Ubuntu’s default apt repository — it ships an outdated version. Install from Docker’s official repository:

# Remove any old Docker installations
sudo apt remove docker docker-engine docker.io containerd runc 2>/dev/null

# Install dependencies
sudo apt update
sudo apt install ca-certificates curl gnupg lsb-release -y

# Add Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add Docker 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 update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

Verify Docker is running:

sudo docker run hello-world

Add Your User to the Docker Group

To run Docker commands without sudo:

sudo usermod -aG docker $USER
newgrp docker

Log out and back in for the group change to take full effect.

Step 2: Configure Docker for Production

Daemon Configuration

Create or edit the Docker daemon configuration file:

sudo nano /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 64000,
      "Soft": 64000
    }
  },
  "live-restore": true
}

Key settings explained:

  • log-driver json-file with size limits prevents container logs from filling your VPS disk
  • live-restore: true allows containers to keep running during Docker daemon restarts (critical for production uptime)
  • Increased nofile ulimits prevent connection exhaustion under high load

Restart Docker to apply:

sudo systemctl restart docker

Enable Docker to Start on Boot

sudo systemctl enable docker
sudo systemctl enable containerd

Step 3: Set Up Docker Networks for Production

Docker’s default bridge network is fine for development but creates security and performance issues in production. Create dedicated networks for your services:

# Frontend network (accessible from Nginx proxy)
docker network create --driver bridge frontend

# Backend network (isolated internal communication)
docker network create --driver bridge backend

This separation ensures your database containers are not exposed on the frontend network — only application containers that need to reach the database join the backend network.

Step 4: Install and Configure Nginx as a Reverse Proxy

Rather than exposing Docker containers directly on ports 80/443, run Nginx on the host as a reverse proxy. This gives you centralized SSL termination, request routing, and logging.

sudo apt install nginx certbot python3-certbot-nginx -y

Create a reverse proxy configuration for your first application:

sudo nano /etc/nginx/sites-available/myapp.conf
upstream myapp {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name myapp.yourdomain.com;

    location / {
        proxy_pass http://myapp;
        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_connect_timeout 60s;
        proxy_read_timeout 60s;
    }
}
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Issue SSL certificate
sudo certbot --nginx -d myapp.yourdomain.com

Step 5: Deploy a Multi-Service Application with Docker Compose

Docker Compose orchestrates multi-container applications with a single configuration file. Here is a production-ready example deploying a Node.js application with Redis and PostgreSQL:

mkdir ~/myapp && nano ~/myapp/docker-compose.yml
version: '3.8'

services:
  app:
    image: myapp:latest
    restart: always
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    networks:
      - frontend
      - backend
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  db:
    image: postgres:16-alpine
    restart: always
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    networks:
      - backend
    deploy:
      resources:
        limits:
          memory: 1G

  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    networks:
      - backend

volumes:
  pgdata:
  redisdata:

networks:
  frontend:
    external: true
  backend:
    external: true

Create the environment file for secrets:

nano ~/myapp/.env
DB_PASSWORD=your_very_strong_password_here
chmod 600 ~/myapp/.env

Start the stack:

cd ~/myapp
docker compose up -d

Verify all services are running:

docker compose ps
docker compose logs --tail=50

Step 6: Persistent Data and Volume Management

Docker volumes persist data beyond container lifecycles. For production databases, always use named volumes (as in the example above) rather than bind mounts. To back up a Docker volume:

# Backup PostgreSQL volume to a file
docker run --rm \
  -v myapp_pgdata:/source \
  -v $(pwd):/backup \
  alpine tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .

# Copy backup off the server
rsync -avz pgdata-*.tar.gz backup@offsite-server:/backups/

Step 7: Update Containers with Zero Downtime

For production updates, pull the new image and restart with a brief downtime window:

cd ~/myapp
docker compose pull app
docker compose up -d --no-deps app

The --no-deps flag restarts only the application container without touching the database or Redis. For zero-downtime deployments, implement a health check in your Compose file and use Docker’s rolling update capability, or consider a more sophisticated orchestration tool like Docker Swarm or Kubernetes.

Step 8: Monitoring Docker Containers

Monitor container resource usage in real time:

# Live resource usage for all containers
docker stats

# Check container health
docker compose ps

# Tail logs from all services
docker compose logs -f --tail=100

For persistent monitoring, deploy cAdvisor and connect it to your Grafana/Prometheus stack, or use Netdata which has built-in Docker container metrics support.

Security Best Practices for Docker on VPS

  • Never run containers as root: Add a non-root user in your Dockerfiles with USER nonroot
  • Use read-only containers where possible: Add read_only: true in Compose for stateless services
  • Bind published ports to localhost: Use 127.0.0.1:PORT:PORT syntax for any port that should only be accessible via the Nginx proxy, not directly from the internet
  • Scan images for vulnerabilities: Use docker scout cves IMAGE before deploying to production
  • Keep images updated: Outdated base images accumulate CVEs. Schedule monthly image rebuilds.
  • Use secrets management: Avoid hardcoding credentials in environment variables for highly sensitive data — use Docker secrets or a vault solution

UFW Firewall and Docker

Docker modifies iptables directly, which can bypass UFW rules. To prevent Docker from bypassing UFW and exposing ports directly:

sudo nano /etc/docker/daemon.json

Add to the existing JSON configuration:

"iptables": false

Then manage all port exposure explicitly through UFW rules. This is the more secure approach for production VPS deployments.

Getting Started

A KVM VPS with at least 2 vCPU and 2 GB RAM is the minimum recommended starting point for a Docker production environment. KVM VPS plans at VPS.DO include full kernel access required for Docker, NVMe storage for fast container I/O, and root access for complete configuration control.

Conclusion

Docker on a VPS provides a portable, reproducible deployment model that dramatically simplifies application lifecycle management. With proper daemon configuration, network isolation, Nginx reverse proxy, volume backups, and security hardening, a Docker-based VPS deployment can handle production workloads reliably. The setup described in this guide — Docker Compose with dedicated networks, host-based Nginx proxy, and automated backups — is a solid foundation for the majority of web application deployments.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!