Skip to content
Tutorials 4 min read

Docker on a VPS: installation and your first container

One configuration file, one command — that is what self-hosting with Docker looks like today. Installing from the official repository, your first Compose container, and the firewall trap many people miss.

Lars Kiefer

Lars Kiefer

Docker has made self-hosting dramatically simpler: what used to cost an evening of installation work now starts from one configuration file and a single command. The installation itself takes ten minutes — provided you use the official repository and know about one nasty trap that catches even experienced admins: Docker ignores your firewall.

What you need

You need a server with root access running Debian or Ubuntu; 2 GB of RAM is enough for your first services. Virtualization matters here: on a true KVM server, Docker runs natively and without restrictions — on container-based VPS products (LXC/OpenVZ) it often works only with contortions, or not at all.

Step 1: Install Docker from the official repository

Distribution packages for Docker are frequently months behind. Install from the official Docker repository instead: it gives you current versions and clean updates through apt. The commands below set it up on Debian (for Ubuntu, replace debian with ubuntu in both URLs):

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/debian/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian $(. /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-compose-plugin

The test container tells you whether everything works:

sudo docker run hello-world

If "Hello from Docker!" appears, your installation is complete — including Docker Compose, which today ships as a plugin (docker compose, no hyphen).

Step 2: Use Docker without sudo (optional)

To avoid prefixing every command with sudo, add your user to the docker group and log in again once:

sudo usermod -aG docker $USER

Be clear about one consequence: members of the docker group can effectively gain root privileges through containers. On a server you administer alone, that is common and acceptable — but group membership for additional users should be treated exactly like handing out root access.

Step 3: Your first real service with Docker Compose

Rather than typing long docker run commands, you describe services in a compose.yaml — reviewable, version-controllable, startable with one command. Uptime Kuma makes a practical first project: a self-hosted monitoring tool with a web interface. Create a folder containing this file:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:3001"
    volumes:
      - ./data:/app/data

Then, in that same folder:

docker compose up -d

Docker pulls the image and starts the service in the background; docker compose logs -f shows what is happening. Thanks to restart: unless-stopped, the container survives a server reboot too. Why the port above is deliberately bound to 127.0.0.1 is the subject of the next section — the most important one in this article.

The trap: Docker bypasses your firewall

If you have secured your server with UFW, you expect that whatever is not allowed is closed. Docker breaks that expectation. A published port (ports: "3001:3001") is written straight into the iptables rules by Docker, bypassing UFW. The service is then reachable from the internet even though your firewall appears to block it. This is how databases and admin panels end up publicly exposed by accident.

The robust fix is the pattern shown above: bind ports to 127.0.0.1 only. The container then listens locally, and you decide what goes outside — properly, through a reverse proxy with TLS, or for your own use through an SSH tunnel:

ssh -L 3001:127.0.0.1:3001 user@server-ip

You can then reach the interface locally at http://localhost:3001 — encrypted by SSH, invisible to the rest of the internet.

Rule of thumb: publish container ports as 127.0.0.1:PORT:PORT by default. A service becomes public through a deliberate decision of yours — never through a Docker default.

Updates and housekeeping

Updating containers takes two commands: docker compose pull fetches new images, docker compose up -d restarts the services with them. Old images stay behind and fill up the disk over time — docker system prune clears out what is unused (check with docker system df first to see what can go). An occasional look at df -h is part of the routine when you host containers.

Conclusion

The official repository, Compose instead of long individual commands, and ports consistently bound to 127.0.0.1 — with those three habits your Docker setup stays clean, maintainable and safe. After the first service, such a server tends to grow quickly; how much CPU and RAM a multi-container setup needs is covered in the server sizing guide. And if it grows further: the AMD EPYC servers offer exactly the profile parallel containers benefit from, with plenty of cores and NVMe storage.

Topics: #Linux #Docker #Container #Self-Hosting
Lars Kiefer

Written by

Lars Kiefer

See for yourself.

Test your server free of charge for 24 hours – set up automatically, with full access to the control panel. It ends on its own afterwards.