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 -dbrings 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.
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
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.
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/tcpto 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: 512mandcpus: "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 trustinglatest. - 🧹 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
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.
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.