What Is Docker, and Why Use It on a VPS?

Docker is a platform that packages and runs applications in lightweight, portable, isolated units called containers. A container bundles your application together with its dependencies, libraries, and configuration into a single image, guaranteeing identical behavior across development, testing, and production environments.

The key difference from virtual machines (VMs): containers share the host operating system's kernel instead of booting their own. As a result, a container starts in milliseconds rather than seconds, and takes up tens of megabytes instead of hundreds. A single VPS can comfortably run dozens of containers.

Concrete benefits of running Docker on a VPS:

  • 🔁 Portability: The same image runs identically on your local machine, on a VPS, or with a completely different provider.
  • 📦 Isolation: It doesn't matter if one app needs PHP 8.3 and another needs PHP 7.4 — each one runs happily in its own container.
  • Fast deployment: A single docker compose up -d brings your entire stack online in seconds.
  • ♻️ Easy rollback: If a new release causes problems, you can revert to the previous image with a single command.
💡 Good to know: You can't run Docker on shared hosting, since it requires root access and specific kernel features. Docker needs at least a VPS or a dedicated server. To understand the difference between a VDS and a VPS, check out our VDS comparison page.

Installing Docker on a VPS (Ubuntu 22.04/24.04)

The steps below install the latest version of Docker from its official repository on Ubuntu 22.04 and 24.04. Run these commands as root or with a sudo-enabled user.

1. Remove old versions and update the system:

# Remove any conflicting older packages
sudo apt-get remove -y docker docker-engine docker.io containerd runc

# Update system packages
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

2. Add Docker's official GPG key and repository:

# Create the GPG key directory
sudo install -m 0755 -d /etc/apt/keyrings

# Download Docker's GPG key
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 the repository to your sources list
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

3. Install Docker Engine and the Compose plugin:

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# Verify the installation
sudo docker --version
sudo docker compose version

4. Add your user to the docker group (so you don't need to type sudo for every command):

sudo usermod -aG docker $USER

# Log out and back in for the change to take effect, or run:
newgrp docker

# Test: this should work without sudo
docker run hello-world
⚠️ Security warning: any user added to the docker group effectively gains root-level privileges, since containers can access the host filesystem. Always use SSH key authentication for this user and disable password login.

Running Your First Container

Let's spin up an Nginx web server with a single command. This demonstrates how port mapping and persistent data (volumes) work:

# -d: run in the background, --name: container name
# -p: host_port:container_port, -v: mount persistent files
docker run -d \
  --name web \
  -p 80:80 \
  -v /var/www/site:/usr/share/nginx/html:ro \
  nginx:latest

# List running containers
docker ps

# Follow the logs
docker logs -f web

# Get a shell inside the container (for debugging)
docker exec -it web /bin/bash

# Stop and remove it
docker stop web && docker rm web

Here, -p 80:80 maps port 80 on the VPS to port 80 inside the container. -v mounts the VPS's /var/www/site folder into the container, so your files survive even if you delete the container. Persistent data management is one of the most critical aspects of Docker — always keep database data on a volume.

Multiple Services with docker-compose

Real-world projects rarely consist of a single container: a web application, a database, and maybe a cache (Redis) all need to run together. docker-compose lets you define all these services in one docker-compose.yml file and manage them with a single command.

Below is an example WordPress + MySQL stack. Create a directory and save this as docker-compose.yml inside it:

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp_user
      MYSQL_PASSWORD: strong_password_123
      MYSQL_ROOT_PASSWORD: much_stronger_root_456
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - internal

  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wp_user
      WORDPRESS_DB_PASSWORD: strong_password_123
      WORDPRESS_DB_NAME: wordpress
    ports:
      - "8080:80"
    volumes:
      - wp_data:/var/www/html
    networks:
      - internal

volumes:
  db_data:
  wp_data:

networks:
  internal:

Commands for managing the stack:

# Start all services in the background
docker compose up -d

# Check status
docker compose ps

# Follow the logs of all services live
docker compose logs -f

# Pull the latest images and restart
docker compose pull && docker compose up -d

# Stop the whole stack (volumes are preserved)
docker compose down

# WARNING: -v also deletes the volumes (data loss!)
# docker compose down -v

Notice that the db and wordpress services sit on the same internal network, so they can reach each other by service name (e.g. db:3306). We didn't expose the database's port (ports) to the outside world — that's a deliberate security choice: MySQL is only reachable from the internal network.

Setting Up an Nginx Reverse Proxy

To publish multiple applications from a single IP address through ports 80/443, you need a reverse proxy. A reverse proxy routes incoming requests to the correct container based on the domain name. The first approach is a classic Nginx configuration.

Example: an Nginx block that routes requests for app1.yoursite.com to the container on port 8080, and app2.yoursite.com to port 8090:

# /etc/nginx/sites-available/app1.conf
server {
    listen 80;
    server_name app1.yoursite.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

Now let's enable the configuration and obtain a free SSL certificate with Let's Encrypt. Certbot automates the whole process:

# Enable the configuration and test it
sudo ln -s /etc/nginx/sites-available/app1.conf \
  /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Install Certbot and obtain an SSL certificate (auto-renews)
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app1.yoursite.com

# Test automatic renewal
sudo certbot renew --dry-run

In this approach, Nginx runs directly on the VPS, outside of any container. It works great for simple setups, but you'll need to manually create a configuration file for every new domain.

Automatic SSL Management with Traefik

Traefik is a modern reverse proxy built specifically for Docker. Its biggest advantage: it reads container labels to configure routing automatically, and it obtains and renews Let's Encrypt certificates on its own. Adding a new application only takes a handful of label lines.

Below is a docker-compose.yml containing Traefik plus a sample web application. Replace the email address with your own:

services:
  traefik:
    image: traefik:v3.1
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      # Automatically redirect HTTP to HTTPS
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      # Automatic Let's Encrypt certificates
      - "--certificatesresolvers.le.acme.email=you@yoursite.com"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.tlschallenge=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt

  app:
    image: nginx:latest
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.yoursite.com`)"
      - "traefik.http.routers.app.entrypoints=websecure"
      - "traefik.http.routers.app.tls.certresolver=le"
      - "traefik.http.services.app.loadbalancer.server.port=80"

Start the stack; Traefik will automatically obtain an SSL certificate for app.yoursite.com and redirect traffic to HTTPS:

docker compose up -d

# Watch the Traefik logs to see the certificate being issued
docker compose logs -f traefik

To add a new application, all you have to do is add those four labels lines to the new service (with your own domain). Traefik takes care of the rest.

✅ Comparison: For a handful of sites and stable setups where manual configuration is fine, Nginx + Certbot is more than enough. For dynamic environments that change frequently, run many containers, and need "zero-touch" SSL, Traefik is far more practical.

The table below offers a quick comparison of the two approaches:

Feature Nginx + Certbot Traefik
Configuration Manual file per site Container labels (automatic)
SSL renewal Via Certbot cron Fully automatic
Learning curve Low (classic) Moderate
Dynamic environments Weak fit Excellent fit

Security and Resource Management

Docker is powerful, but misconfiguration turns it into a security liability. Here's what to watch out for in production:

  • 🔒 Don't expose unnecessary ports: Only open 80 and 443 to the outside world. Keep databases and internal services confined to the Docker network (in the example above, the MySQL port was never exposed).
  • 🧱 Firewall (UFW): Use sudo ufw allow 22,80,443/tcp to allow only the ports you actually need. Getting Docker and UFW to cooperate correctly may require some extra configuration.
  • 📉 Set resource limits: Prevent a single container from consuming all your RAM and crashing the VPS. Add mem_limit: 512m and cpus: "0.5" to the service in your compose file.
  • 🔄 Keep images up to date: Pull security patches regularly with docker compose pull. Use pinned version tags (e.g. mysql:8.0) rather than blindly trusting latest.
  • 🧹 Clean up disk space: Unused images and volumes will quietly bloat your disk over time.

Commands for monitoring resource usage and cleaning up:

# Live CPU/RAM usage per container
docker stats

# View disk usage
docker system df

# Clean up unused images, containers, and networks (use with caution)
docker system prune -a

# Remove only dangling images
docker image prune
⚠️ Backups: database data stored in a volume survives even if you delete the container — but not if the VPS itself goes down. Regularly back up critical data to somewhere outside the VPS. For a backup strategy, check out our VPS management guide.

Which VPS Should You Choose for Docker?

Docker itself consumes very few resources — the real load comes from the applications you run inside it. Still, as a healthy starting point, aim for roughly these specs:

  • Testing / small projects: 1 vCPU, 2 GB RAM, 40 GB NVMe SSD. Enough for a few lightweight containers (a static site, a small API). In Turkey, this typically runs ~₺200-350 per month.
  • WordPress + MySQL + Redis stack: 2 vCPU, 4 GB RAM, 80 GB NVMe. Comfortably handles a single medium-traffic project, in the ~₺400-600 per month range.
  • Multiple projects / Traefik with several services: 4 vCPU, 8 GB RAM, 160 GB NVMe. Budget for around ~₺750-1,200 per month.

An NVMe SSD noticeably speeds up pulling Docker images and starting containers. Being generous with RAM becomes critical once you're running many containers at once. You can browse current pricing and specs on our VPS comparison page, check our VDS comparison for heavier workloads, and put different providers side by side with our comparison tool.

💡 Tip: while you're learning Docker, grab the cheapest VPS available and experiment freely. If you break something, just delete the container and spin up a clean copy in seconds with docker compose up -d — that forgiving nature is one of Docker's biggest learning advantages.

Frequently Asked Questions

What's the difference between Docker and a virtual machine (VM)?

A virtual machine virtualizes an entire operating system with its own kernel, so it's heavy and slow to boot. A Docker container, on the other hand, shares the host's kernel and only packages the application and its dependencies. That's why containers start in milliseconds and use far less RAM and disk space — a single VPS can comfortably run dozens of them.

How much RAM does Docker need?

Docker itself gets by on a few hundred MB — what really matters is the applications you run. 2 GB of RAM is reasonable for a single WordPress + MySQL stack, though 4 GB is recommended for comfortable headroom. For multiple projects or memory-hungry services (like Elasticsearch), aim for 8 GB or more.

Are docker-compose and Docker Compose the same thing?

The old docker-compose (with a hyphen) was a separate Python tool. In newer versions, that functionality was folded into Docker itself as a plugin, invoked with docker compose (with a space). The installation in this guide sets up the current version via the docker-compose-plugin package — the YAML syntax is identical either way.

Can I publish containers without a reverse proxy?

Technically, yes — you can bind each container to a different port (8080, 8090...) and access it directly through those ports. But users expect clean domain names (app.yoursite.com) and HTTPS. To publish multiple sites through port 443 on a single IP and manage SSL certificates centrally, a reverse proxy like Nginx or Traefik is nearly essential.

If I delete a container, do I lose my data?

Temporary data stored in the container's own layer is lost, but anything you've mounted through a volume (such as a MySQL database) is preserved. That's why anything that needs to persist should always live on a named volume or a host directory. That said, don't skip regular backups to somewhere outside the VPS, in case the VPS itself goes down entirely.