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
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
| Command | Description |
|---|---|
| docker ps -a | All containers |
| docker images | List images |
| docker logs -f name | Container logs (live) |
| docker exec -it name bash | Enter the container |
| docker stats | Resource usage |
| docker system prune | Clean up unused data |
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
| Parameter | Docker container | Installing directly on the host |
|---|---|---|
| Dependency isolation | Full, its own library versions | Shared with the system, version conflicts possible |
| Moving to another server | The image moves as-is | Installation and setup have to be repeated |
| Resource usage | A small overhead for the engine | Minimal, no extra layer |
| Low-level debugging | Harder, requires entering the container | Direct 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-USERchain 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
- Check that the daemon starts automatically:
systemctl is-enabled dockershould returnenabled. - Confirm an unprivileged user can run docker commands without sudo:
docker psfrom a regular user should not ask for a password or fail with a permissions error. - Check the actual reachability of the published port from outside, not just via
curl http://localhoston the server itself. - Check the firewall rules for container ports:
iptables -L DOCKER-USER -nshows what is actually being filtered at the Docker level, separately from the output ofufw status.