How to Deploy Docker Containers on a VPS: A Beginner-Friendly Guide
Docker is the most popular way to package and deploy applications on a VPS in 2025. Instead of manually installing dependencies, fighting version conflicts, and writing setup scripts, you bundle your entire app — code, runtime, libraries — into a container that runs identically on any server.
This guide covers everything from installing Docker to running a multi-service app with Docker Compose. No prior Docker experience needed.
Key Concepts Before We Start
Install Docker from the official Docker repository — this gives you the latest version with security patches, unlike the older version in Ubuntu’s default repos.
# Remove old Docker versions if any $ apt remove docker docker-engine docker.io containerd runc -y # Install dependencies $ apt update $ apt install ca-certificates curl gnupg -y # Add Docker's official GPG key $ install -m 0755 -d /etc/apt/keyrings $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg $ 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 \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine $ apt update $ apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin -y
$ docker --version # Docker version 26.x.x # Run the hello-world test container $ docker run hello-world
Allow non-root user to run Docker
# Add your user to the docker group (no sudo needed) $ usermod -aG docker $USER $ newgrp docker # Verify — should run without sudo $ docker ps
Let’s deploy a real app — Nginx — as a Docker container to see how it works:
# Pull the official Nginx image from Docker Hub $ docker pull nginx # Run it — map port 8080 on the host to port 80 inside the container $ docker run -d \ --name my-nginx \ -p 8080:80 \ nginx # Check it's running $ docker ps
Visit http://your-server-ip:8080 — you’ll see the Nginx welcome page served from inside a container.
Basic container management commands
$ docker stop my-nginx # Stop the container $ docker start my-nginx # Start it again $ docker restart my-nginx # Restart $ docker rm my-nginx # Remove container (must be stopped) $ docker logs my-nginx # View container logs $ docker exec -it my-nginx bash # Open a shell inside the container
Essential concepts
These three flags are the building blocks of almost every docker run command you’ll ever write:
-p: Port Mapping (Host:Container)
# -p HOST_PORT:CONTAINER_PORT $ docker run -p 3000:3000 my-node-app # Bind to a specific IP (more secure — only localhost can reach it) $ docker run -p 127.0.0.1:3000:3000 my-node-app
127.0.0.1 so the container port is only accessible from localhost — then use Nginx as a reverse proxy (covered in our previous guide) to expose it publicly over HTTPS.-v: Volume Mounting (Persistent Data)
# Mount a host directory into the container # -v /host/path:/container/path $ docker run -v /var/www/html:/usr/share/nginx/html nginx # Named volume (Docker manages the storage location) $ docker run -v my-db-data:/var/lib/mysql mysql
-e: Environment Variables
# Pass configuration to containers without hardcoding secrets $ docker run -d \ -e DB_HOST=localhost \ -e DB_PASSWORD=secret123 \ -e NODE_ENV=production \ my-app
Docker Compose lets you define multiple containers in a single
docker-compose.yml file and manage them as a unit. It’s essential for running real applications that need a database, cache, or background worker alongside the main app.# Docker Compose V2 is included with the Docker installation # If you installed Docker via the official repo, it's already there: $ docker compose version # Docker Compose version v2.x.x # If not installed, add the plugin manually: $ apt install docker-compose-plugin -y
docker compose (no hyphen). The older V1 used docker-compose (with hyphen). This guide uses V2 syntax throughout.Here’s a real-world example: a Node.js app + PostgreSQL database + Redis cache, all defined in one Compose file. This is the kind of stack that powers most production web applications.
services: app: image: node:20-alpine container_name: my-app working_dir: /app volumes: - ./app:/app # Mount local code into container ports: - "127.0.0.1:3000:3000" # Expose only to localhost environment: - NODE_ENV=production - DB_HOST=postgres # Service name = hostname - DB_PASSWORD=${DB_PASSWORD} # Read from .env file - REDIS_HOST=redis command: node server.js depends_on: - postgres - redis restart: unless-stopped networks: - app-network postgres: image: postgres:16-alpine container_name: my-postgres environment: - POSTGRES_DB=myapp - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - postgres-data:/var/lib/postgresql/data # Persist DB data restart: unless-stopped networks: - app-network redis: image: redis:7-alpine container_name: my-redis restart: unless-stopped networks: - app-network volumes: postgres-data: # Named volume — data survives container restarts networks: app-network: driver: bridge
Create a .env file in the same directory to store secrets:
DB_PASSWORD=your-super-secret-password
Start, stop, and manage your stack
# Start all services in the background $ docker compose up -d # View running services $ docker compose ps # View logs (all services, or one) $ docker compose logs -f $ docker compose logs -f app # Stop all services $ docker compose down # Stop and remove volumes (⚠️ deletes DB data) $ docker compose down -v
postgres:5432 — no IP addresses needed.Keep Containers Running After Reboot
Production must-do
By default, Docker containers stop when your server reboots. There are two ways to fix this:
Option A: restart policy (simplest)
Add restart: unless-stopped to each service in your Compose file (already included in the example above). This restarts containers automatically after a reboot or crash.
Option B: Enable Docker to start on boot
# Enable Docker daemon to start on system boot $ systemctl enable docker # Verify $ systemctl is-enabled docker # enabled
With Docker enabled on boot and restart: unless-stopped in your Compose file, your entire stack will automatically come back online after any server restart.
Essential Docker Commands Cheat Sheet
Bookmark this
| Command | What it Does |
|---|---|
docker ps |
List running containers |
docker ps -a |
List all containers (including stopped) |
docker images |
List downloaded images |
docker pull nginx |
Download an image from Docker Hub |
docker logs -f name |
Stream container logs in real time |
docker exec -it name bash |
Open interactive shell in container |
docker stats |
Live CPU/RAM usage per container |
docker system prune |
Remove stopped containers, unused images |
docker compose up -d |
Start all Compose services detached |
docker compose pull |
Pull latest images for all services |
docker compose restart |
Restart all services |
docker volume ls |
List all volumes |
Docker installed from official repo (not Ubuntu’s default package)
Non-root user added to docker group
Docker daemon enabled on boot (systemctl enable docker)
App ports bound to 127.0.0.1 — not exposed directly to the internet
Nginx reverse proxy fronting all containers (HTTPS on port 443)
Secrets stored in .env file, not hardcoded in docker-compose.yml
Named volumes used for all persistent data (databases, uploads)
restart: unless-stopped on all production services
Regular volume backups configured
docker system prune scheduled weekly to reclaim disk space
Frequently Asked Questions
docker compose pull && docker compose up -d. This pulls the latest images and recreates only the containers whose images have changed — with minimal downtime.docker run --rm -v postgres-data:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz /data to create a compressed archive of a named volume. For PostgreSQL specifically, use docker exec my-postgres pg_dumpall -U postgres > backup.sql for a proper SQL dump.🐳 You’re Ready to Ship with Docker
Docker transforms your VPS from a bare server into a flexible deployment platform. You can now spin up any application in minutes, run multiple services side by side without conflicts, and update or roll back deployments with a single command.
Next steps: write a Dockerfile to containerize your own application, set up a CI/CD pipeline (GitHub Actions → Docker Hub → VPS deploy), and explore Docker Swarm or Portainer for a management UI if you prefer not working from the command line.