How to Install Docker Compose on VPS (2025) — Complete Guide

What Is Docker Compose and Why You Need It

Docker Compose is a tool for defining and running multi-container Docker applications. Instead of typing long docker run commands with dozens of flags, you describe your entire application stack in a single YAML file called docker-compose.yml. For Docker fundamentals, see Docker on Ubuntu VPS. One command — docker compose up -d — creates all the containers, networks, and volumes your application needs. For hosting recommendations, see VPS for Docker and VPS for Self-Hosting.

This is essential for modern application deployments. Most real-world applications consist of multiple services: a web server, an application backend, a database, a cache layer, and perhaps a message queue. Managing these as individual containers with separate docker run commands is error-prone and hard to reproduce. Docker Compose solves this by treating the entire stack as a single unit. For automation use cases, see Setup n8n on VPS and Deploy FastAPI on VPS.

Docker Compose V2 (the current version) is installed as a Docker CLI plugin. You invoke it as docker compose (with a space) rather than the older standalone docker-compose binary (with a hyphen). The V1 standalone binary is deprecated and no longer receives updates. All modern Docker installations include the Compose V2 plugin by default.

Prerequisites

RequirementDetails
Operating SystemUbuntu 22.04 LTS or 24.04 LTS
Root AccessSSH access with root privileges or a sudo user
RAMMinimum 1GB (2GB+ recommended for multi-container stacks)
Disk SpaceMinimum 10GB free for images and container data

Step 1: Install Docker Engine

Docker Compose requires Docker Engine as a prerequisite. Install Docker CE from the official Docker repository to get the latest stable version.

Connect to your VPS and update the system.

ssh root@your-vps-ip
apt update && apt upgrade -y

Install the prerequisites for adding a third-party repository.

apt install -y ca-certificates curl gnupg lsb-release apt-transport-https software-properties-common

Add Docker's official GPG key.

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

Add the Docker APT repository.

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update

Step 2: Install Docker Compose Plugin

Install Docker Engine along with the Compose V2 plugin. The Compose plugin is bundled as docker-compose-plugin and integrates directly into the Docker CLI.

apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Enable and start Docker.

systemctl enable docker
systemctl start docker

If you want to run Docker commands without sudo, add your user to the docker group.

usermod -aG docker $USER
su - $USER

Step 3: Verify Installation

Check that both Docker Engine and Docker Compose are installed correctly.

docker --version
docker compose version

You should see output similar to:

Docker version 27.x.x, build xxxxxxx
Docker Compose version v2.x.x

Run a quick test to confirm Docker is fully operational.

docker run --rm hello-world

If you see the "Hello from Docker!" message, both Docker Engine and the Compose plugin are working correctly.

Step 4: Create Your First docker-compose.yml

Create a project directory and your first Docker Compose file. This example defines a simple web application with Nginx serving static content.

mkdir -p /opt/myapp && cd /opt/myapp
cat > docker-compose.yml << 'EOF'
services:
  web:
    image: nginx:alpine
    container_name: myapp-web
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
    restart: unless-stopped
EOF

Create a test HTML file.

mkdir -p html
cat > html/index.html << 'EOF'


My Docker Compose App

Hello from Docker Compose!

This is running on Nginx inside a Docker container.

EOF

Start the application.

docker compose up -d

Visit http://your-vps-ip to see the page. Docker Compose pulled the Nginx Alpine image, created a container, and started it — all from a single YAML file.

Stop and remove the containers when you are done.

docker compose down

Step 5: Multi-Container Application Example

This is where Docker Compose truly shines. The following example defines a complete application stack with Nginx (reverse proxy), a Python Flask application, and a PostgreSQL database.

cat > docker-compose.yml << 'EOF'
version: "3.9"

services:
  # Nginx reverse proxy
  nginx:
    image: nginx:alpine
    container_name: app-nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network

  # Flask application
  app:
    build: ./app
    container_name: app-flask
    environment:
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
      - FLASK_ENV=production
    expose:
      - "5000"
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:16-alpine
    container_name: app-postgres
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
      POSTGRES_DB: appdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  postgres_data:

networks:
  app-network:
    driver: bridge
EOF

Create the Flask Application

mkdir -p app
cat > app/Dockerfile << 'EOF'
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
EOF
cat > app/requirements.txt << 'EOF'
flask==3.0.0
gunicorn==21.2.0
psycopg2-binary==2.9.9
EOF
cat > app/app.py << 'EOF'
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify({"message": "Hello from Docker Compose!"})

@app.route('/health')
def health():
    return jsonify({"status": "healthy"}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
EOF

Create the Nginx Configuration

mkdir -p nginx
cat > nginx/default.conf << 'EOF'
upstream flask_app {
    server app:5000;
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://flask_app;
        proxy_http_version 1.1;
        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;
    }
}
EOF

Build and start the entire stack.

cd /opt/myapp
docker compose up -d --build

Docker Compose builds the Flask image, pulls Nginx and PostgreSQL images, creates the network and volume, and starts all three services in dependency order. The depends_on with condition: service_healthy ensures the database is ready before the app starts, and the app is healthy before Nginx begins routing traffic to it.

Step 6: Managing Containers

Docker Compose provides a complete set of commands for managing the lifecycle of your application stack.

Starting and Stopping

# Start all services in detached mode
docker compose up -d

# Start with verbose output (foreground, useful for debugging)
docker compose up

# Stop all services (containers are preserved)
docker compose stop

# Stop and remove all containers, networks
docker compose down

# Stop and remove containers, networks, AND volumes (deletes database data!)
docker compose down -v

# Restart all services
docker compose restart

# Restart a single service
docker compose restart app

Viewing Status and Logs

# View status of all services
docker compose ps

# View logs for all services
docker compose logs -f

# View logs for a specific service
docker compose logs -f app

# View last 100 lines of logs
docker compose logs --tail=100 app

Scaling Services

# Scale a service to multiple instances
docker compose up -d --scale app=3

Note: Scaling with Docker Compose works well for stateless services. Stateful services like databases should not be scaled this way. For production scaling, consider using Docker Swarm or Kubernetes.

Step 7: Environment Variables and .env Files

Environment variables allow you to configure your application without modifying the docker-compose.yml file. Docker Compose supports multiple ways to define and inject environment variables.

Using a .env File

Create a .env file in the same directory as your docker-compose.yml. Docker Compose automatically reads this file.

cat > .env << 'EOF'
POSTGRES_USER=appuser
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_DB=appdb
FLASK_ENV=production
APP_VERSION=1.0.0
EOF

Reference these variables in your docker-compose.yml using the ${VARIABLE_NAME} syntax.

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Variable Substitution with Defaults

You can provide default values that are used when the variable is not set.

environment:
  - FLASK_ENV=${FLASK_ENV:-development}
  - LOG_LEVEL=${LOG_LEVEL:-info}
  - WORKERS=${WORKERS:-2}

Environment-Specific Compose Files

Create separate override files for different environments. Docker Compose merges multiple compose files automatically.

# Production overrides (docker-compose.prod.yml)
services:
  app:
    environment:
      - FLASK_ENV=production
    restart: always
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
# Use: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Development overrides (docker-compose.dev.yml)
services:
  app:
    volumes:
      - ./app:/app  # Hot-reload source code
    environment:
      - FLASK_ENV=development
# Use: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Passing Variables at Runtime

# Set environment variables inline
POSTGRES_PASSWORD=secretpass docker compose up -d

# Or use --env-file to specify a custom .env file
docker compose --env-file .env.production up -d

Security Warning: Never commit your .env file to version control. Add .env to your .gitignore file. Use .env.example (without real values) as a template for other developers.

Step 8: Networks and Volumes

Docker Compose Networks

Every Docker Compose project automatically creates a default bridge network. All services in the compose file can communicate with each other using their service names as hostnames. You can also define custom networks for more complex topologies.

services:
  frontend:
    image: nginx:alpine
    networks:
      - front-tier
      - back-tier

  backend:
    image: node:20-alpine
    networks:
      - back-tier

  database:
    image: postgres:16-alpine
    networks:
      - back-tier

networks:
  front-tier:
    driver: bridge
  back-tier:
    driver: bridge
    internal: true  # No outbound internet access

In this example, the frontend can reach both the frontend and backend tiers, but the backend and database are isolated on an internal network with no internet access. This is a security best practice for databases and internal services.

Docker Compose Volumes

Volumes persist data beyond the lifecycle of a container. When you run docker compose down, container data is deleted. Named volumes survive this operation.

services:
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data    # Named volume (managed by Docker)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro  # Bind mount (file)
      - ./backups:/backups                          # Bind mount (directory)

  app:
    image: node:20-alpine
    volumes:
      - app_data:/app/data                          # Named volume
      - ./config:/app/config:ro                     # Read-only bind mount

volumes:
  postgres_data:
    driver: local
  app_data:
    driver: local

Manage volumes with Docker Compose commands.

# List volumes in the current project
docker compose volume ls

# Create volumes without starting services
docker compose volume create postgres_data

# Remove unused volumes
docker compose volume prune

Step 9: Production Tips

Restart Policies

Choose the appropriate restart policy for each service based on its role in your stack.

services:
  web:
    restart: unless-stopped   # Recommended for most services
    # Restarts unless explicitly stopped with docker compose stop

  db:
    restart: always           # Database should always restart
    # Restarts even after the Docker daemon restarts

  migration:
    restart: "no"             # One-time tasks should not restart
    # Run once and stay stopped if they exit

Health Checks

Health checks allow Docker to monitor the actual health of your services, not just whether the process is running. This is critical for dependency management — you want your app to wait for the database to be truly ready, not just have its container started.

services:
  app:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s      # Check every 30 seconds
      timeout: 5s        # Timeout after 5 seconds
      retries: 3         # Fail after 3 consecutive failures
      start_period: 15s  # Grace period before first check

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5

Resource Limits

Prevent any single container from consuming all available resources on your VPS.

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M     # Maximum memory usage
          cpus: '1.0'      # Maximum CPU (1 core)
        reservations:
          memory: 128M     # Guaranteed minimum memory
          cpus: '0.25'     # Guaranteed minimum CPU

Logging Configuration

By default, Docker stores logs indefinitely, which can fill your VPS disk. Configure log rotation to prevent this.

services:
  app:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

This limits each log file to 10MB and keeps a maximum of 3 rotated log files per container, using at most 30MB per service.

Read-Only Root Filesystem

For improved security, run containers with a read-only root filesystem and only allow writes to specific directories.

services:
  web:
    image: nginx:alpine
    read_only: true
    tmpfs:
      - /tmp
      - /var/run
      - /var/cache/nginx

Step 10: Common Commands Reference

Here is a quick reference for the Docker Compose commands you will use most frequently on your VPS.

CommandDescription
docker compose up -dBuild, create, and start all services in detached mode
docker compose downStop and remove containers, networks
docker compose down -vStop and remove containers, networks, and volumes
docker compose psList running services
docker compose logs -fFollow logs for all services
docker compose logs -f appFollow logs for a specific service
docker compose restart appRestart a specific service
docker compose pullPull the latest images for all services
docker compose up -d --buildRebuild images and restart services
docker compose exec app bashExecute a command in a running service
docker compose run app python manage.py migrateRun a one-off command in a new container
docker compose configValidate and view the merged configuration
docker compose topDisplay running processes in each service
docker compose imagesList images used by services
docker compose volume lsList volumes in the project

Frequently Asked Questions

What is the difference between Docker Compose V1 and V2?

Docker Compose V1 was a standalone Python binary invoked as docker-compose (with a hyphen). V2 is a Go-based plugin integrated into the Docker CLI, invoked as docker compose (with a space). V1 is deprecated. V2 is faster, better maintained, and included with all modern Docker installations.

Can I run Docker Compose without Docker Engine?

No. Docker Compose is a tool that manages Docker containers. It requires Docker Engine (the Docker daemon) to be installed and running. Compose sends instructions to the Docker Engine API to create and manage containers.

How do I update Docker Compose?

Docker Compose V2 is updated along with Docker Engine. Run apt update && apt upgrade -y to update both. If you installed Compose separately, update it by downloading the latest binary from the Docker GitHub releases page.

Can I use Docker Compose in production?

Yes, with proper configuration. Use health checks, restart policies, resource limits, log rotation, and read-only filesystems. For large-scale production, consider Docker Swarm or Kubernetes, but Docker Compose is perfectly suitable for single-server production deployments.

How do I back up Docker Compose volumes?

Use Docker to create a tar archive of the volume contents: docker run --rm -v volume_name:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .. Automate this with a cron job for regular backups.

How do I share environment variables between services?

Define the variables at the service level with environment:, or use an env_file: directive to load variables from a file. Variables defined at the top level of the compose file can be referenced by all services using ${VAR_NAME}.

Can Docker Compose manage containers across multiple servers?

Standard Docker Compose manages containers on a single host. For multi-server deployments, use Docker Swarm mode (which supports the Compose file format) or Kubernetes. There are also third-party tools like Podman Compose that can work across hosts.

How do I debug a failing Docker Compose service?

Check the logs with docker compose logs -f service_name. Inspect the container with docker compose exec service_name sh. Verify the health check endpoint is reachable. Use docker compose config to validate the merged configuration. Check resource limits with docker stats.

Need a VPS for Your Docker Compose Stack?

Inferno VPS with NVMe SSD and dedicated resources — run multi-container applications with fast storage I/O and 10Gbps networking.

Get Your VPS →