Skip to main content

Docker on a VPS: Installing and Running Containers

VDS / VPS · 15.03.2025 · 5 min read
Illustration for “Docker on a VPS: Installing and Running Containers”

Docker packages an application together with its dependencies into an isolated container that runs the same way on any machine with the engine installed. On a VPS this removes conflicts between PHP or Node.js versions across different projects on the same server.

Installing Docker via the official script

curl -fsSL https://get.docker.com | sh
systemctl enable --now docker

# Add the user to the docker group (no sudo needed)
usermod -aG docker $USER
newgrp docker

The official script detects the distribution itself and hooks up the right repository — for production, an explicit install following the docs.docker.com instructions for a specific Ubuntu or Debian version is preferable, so the package is pinned instead of pulling whatever the script's latest version happens to be on each new server.

Installing Docker Compose v2

apt install docker-compose-plugin -y
docker compose version
💡 Docker Compose v2 is built in as a plugin (docker compose); the deprecated v1 is a separate binary (docker-compose). Use v2.

First container

# Run Nginx in a container
docker run -d \
  --name mynginx \
  -p 80:80 \
  -v /var/www/html:/usr/share/nginx/html:ro \
  nginx:alpine

# Check
docker ps
curl http://localhost

Main Docker commands

CommandDescription
docker ps -aAll containers
docker imagesList images
docker logs -f nameContainer logs (live)
docker exec -it name bashEnter the container
docker statsResource usage
docker system pruneClean up unused data
⚠️ Do not run containers as root (--user 0) in production. Create a dedicated user inside the image.

Common Errors Explained

The error docker: permission denied while trying to connect to the Docker daemon socket shows up when a user is not in the docker group yet, or the group change has not taken effect in the current session. The command newgrp docker or logging back in over SSH fixes it without rebooting the server.

docker: Error response from daemon: driver failed programming external connectivity on endpoint mynginx: Bind for 0.0.0.0:80 failed: port is already allocated

This error means port 80 on the host is already taken — either by another container, or by a system Nginx or Apache running outside Docker. Check with ss -tulpn | grep :80 before starting the container, rather than debugging a confusing failure afterward.

A container exits right after starting (status Exited (0) or with a non-zero code) usually means the main process inside the container finished and exited, and Docker does not keep a container alive by itself. The cause is visible in the logs: docker logs mynginx shows exactly what went wrong at startup — from a missing config to an error in the entrypoint script.

Docker and the Firewall: an Important Detail

Docker writes its own rules directly into iptables, bypassing UFW. A published container port (-p 8080:8080) may stay reachable from outside even with ufw deny 8080 in place — Docker's rules in the DOCKER-USER chain are applied before UFW's policy gets a chance to act. If you need to control access to container ports through the firewall, the rules need to go into the DOCKER-USER chain specifically, not through plain ufw — otherwise you get a false sense of security, as covered in the article on configuring UFW on a VDS.

Comparing Docker to a Plain Application Install

ParameterDocker containerInstalling directly on the host
Dependency isolationFull, its own library versionsShared with the system, version conflicts possible
Moving to another serverThe image moves as-isInstallation and setup have to be repeated
Resource usageA small overhead for the engineMinimal, no extra layer
Low-level debuggingHarder, requires entering the containerDirect access to processes and files

When Docker Does Not Fit

  • A single application runs on the entire VPS with no plans to scale — the overhead of learning Docker may not pay off compared to a plain package install from the repository.
  • The application needs direct access to host devices or the kernel (specific drivers, low-level networking) — containerizing that requires privileged mode, which undermines the very isolation Docker is meant to provide.
  • The team is not ready to manage the firewall at the DOCKER-USER chain level — otherwise publishing container ports can quietly open more than UFW appears to allow.
  • You need maximum disk I/O performance with no extra layer — Docker's overlay filesystem adds overhead compared to writing directly to the host filesystem.

Verifying the Result

  1. Check that the daemon starts automatically: systemctl is-enabled docker should return enabled.
  2. Confirm an unprivileged user can run docker commands without sudo: docker ps from a regular user should not ask for a password or fail with a permissions error.
  3. Check the actual reachability of the published port from outside, not just via curl http://localhost on the server itself.
  4. Check the firewall rules for container ports: iptables -L DOCKER-USER -n shows what is actually being filtered at the Docker level, separately from the output of ufw status.
Was this article helpful?
← Back to Knowledge Base Ask Support