Initech
KB-23 Print

Run Docker on a VPS: Install and First Container

Quick answer

Install Docker from its official repository, not from snap or the distro's default packages, add your user to the docker group, and you are one command away from running services in containers. This guide covers Ubuntu and Debian on any Linux VPS, ends with a real nginx container, and flags the one firewall trap every VPS user should know about.

Step 1: Install from Docker's official repository

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

On Debian, replace "ubuntu" with "debian" in the two download URLs. The official repository matters because it delivers current versions and the compose plugin together; snap and older distro packages lag behind and differ in paths and behavior.

Step 2: Run Docker without sudo

sudo usermod -aG docker $USER

Log out and back in for the group change to take effect. One honest warning: membership in the docker group is effectively root access on the machine, because containers can mount the host filesystem. Give it only to accounts you would trust with sudo.

Step 3: Hello world

docker run hello-world

If that prints a greeting, the engine works. Now something useful.

Step 4: A real service

Run nginx serving a directory of files from your home directory:

mkdir -p ~/site && echo "hello from a container" > ~/site/index.html
docker run -d --name web --restart unless-stopped -p 8080:80 -v ~/site:/usr/share/nginx/html:ro nginx

Reading that command left to right:

  • -d runs the container in the background; --name web gives it a handle for later commands like docker logs web or docker stop web.
  • --restart unless-stopped brings the container back after a crash or a server reboot. On a VPS this is the policy you want for anything that should stay up.
  • -p 8080:80 publishes container port 80 on host port 8080, so the site answers at http://your-server-ip:8080.
  • -v ~/site:/usr/share/nginx/html:ro mounts your directory into the container read-only. Containers are disposable; data you care about belongs in mounts or named volumes, never only inside the container.

Step 5: Compose, briefly

The compose plugin you installed reads a docker-compose.yml file and manages multi-container setups declaratively. The same nginx service as a compose file:

services:
  web:
    image: nginx
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro

Then docker compose up -d starts it and docker compose down removes it. Once a setup involves two containers or any configuration you want in version control, compose is the way.

The firewall trap: Docker bypasses UFW

This catches almost everyone once. Docker writes its own iptables rules, and a port published with -p is reachable from the internet even if UFW says the port is denied. Your UFW rules are not consulted for published container ports. Two practical defenses: publish onto localhost only, for example -p 127.0.0.1:8080:80, whenever a service should only be reached through a reverse proxy on the same machine; and treat every -p you type as a public exposure decision. The background is covered in our UFW firewall guide.

Cleanup

Images and stopped containers accumulate and quietly eat disk on small servers. Periodically:

docker system df
docker system prune

The first command shows what is using space, the second removes stopped containers, unused networks and dangling images after a confirmation. Add -a to also remove unused images, at the cost of re-downloading them next time.

Updating containers

Containers do not update themselves, and a weekly habit of pulling images for internet-facing services keeps known vulnerabilities from accumulating. For a container started with docker run, pull the new image and recreate it:

docker pull nginx
docker stop web && docker rm web
docker run -d --name web --restart unless-stopped -p 8080:80 -v ~/site:/usr/share/nginx/html:ro nginx

Because the content lives in the mount, recreating is safe. Under compose the same operation is docker compose pull followed by docker compose up -d, which is one more reason to move to compose early.

Logs and troubleshooting

Three commands cover most container debugging on a VPS:

docker logs -f web
docker exec -it web sh
docker inspect web

The first streams the container's output, the second opens a shell inside it, and the third dumps its full configuration, including mounts, ports and restart policy, which is where mismatches hide. If a container restarts in a loop, docker logs almost always says why, and the exit codes shown by docker ps -a are the other clue: 137 usually means the kernel killed the container for running out of memory, a common event on small servers.

Where to go next

With the engine, compose and the firewall rule of thumb in place, most self-hosted software is a compose file away. Keep host mounts or named volumes for anything you would miss, give every long-running service a restart policy, and check docker logs before assuming a container is healthy.

Was this answer helpful?
Related Articles