Private customers Business customers

Install Docker on your VPSDocker Engine and Compose on an LXC container

Docker runs on our VPS plans - nesting is enabled on our LXC containers. This guide shows how to install Docker Engine and Docker Compose, a first stack made of reverse proxy and application, and the specifics you should know on a container-based VPS.

Docker EngineDocker ComposeLXC with nesting20 minutes

Published on

Docker on an LXC VPS: what works and what does not

Our VPS plans use LXC container virtualization. Unlike a full KVM machine, all containers share the kernel of the host system. For Docker, that is generally no obstacle, because Docker containers themselves are just processes with an isolated namespace - not real virtual machines.

For Docker to run inside an LXC container, however, nesting must be enabled. That is the case on our VPS plans; you do not need to request or change anything.

Works

  • Docker Engine and Docker Compose
  • Images from Docker Hub and your own registries
  • Building your own images with a Dockerfile
  • Volumes, networks, port mapping
  • Automatic restart via restart policy

Does not work

  • Nested virtualization, i.e. your own VMs via KVM inside the container
  • Containers that want to load their own kernel modules
  • Images that strictly require a Windows kernel

For the vast majority of use cases - web applications, databases, reverse proxies, bots, automation - that is entirely sufficient. More on the difference in the guide LXC or KVM.

Rent a VPS from €2.95 / month

Prepaid without a contract, full root access.

View plans & prices

Prerequisites

Before you start, the VPS should be up to date and secured at a basic level. If you have not done so yet, work through the first steps and SSH hardening first.

On choosing a plan: Docker itself uses hardly any resources, but the containers inside it certainly do. As a guideline:

  • Plan S: one or two small containers, such as a Discord bot or a static web server.
  • Plan M: a typical stack of reverse proxy, application and database.
  • Plan L or XL: several stacks in parallel, development environments or memory-hungry applications such as Nextcloud with full-text search.

Do not underestimate disk space: Docker images quickly take up several gigabytes, and old layers stay behind unless you clean up.

Install Docker Engine

The version in the standard Debian and Ubuntu package sources is usually outdated. So use the official Docker package sources. First, the preparation:

sudo apt update
sudo apt install ca-certificates curl gnupg -y
sudo install -m 0755 -d /etc/apt/keyrings

Then fetch the signing key. For Debian:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

For Ubuntu, replace debian with ubuntu in both lines. Then install:

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

The test:

sudo docker run --rm hello-world

If the welcome message appears, Docker is running. The --rm parameter removes the test container straight away.

Use Docker without sudo

So that you do not have to prefix every command with sudo, add your user to the docker group:

sudo usermod -aG docker $USER

The change only takes effect after logging in again - so log out and back in. After that, docker ps works without sudo.

Be aware: members of the docker group can effectively gain root rights over the whole system via containers. Only add users you would give these rights to anyway.

Your first Compose stack

Individual docker run commands are handy for tests but unwieldy in permanent operation. Docker Compose instead describes the whole stack in one file that you can version and reuse.

Create a directory with a compose.yaml in it:

mkdir -p ~/stacks/demo && cd ~/stacks/demo
nano compose.yaml

A minimal example with web server and database:

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

  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: please-use-a-long-random-password
      MARIADB_DATABASE: demo
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:

Start, check status, view logs:

docker compose up -d
docker compose ps
docker compose logs -f web

Two details in this file matter more than they seem. restart: unless-stopped makes sure the containers come back up on their own after the server restarts. And the port definition 127.0.0.1:8080:80 deliberately binds the service to localhost only - more on that in a moment.

Ports, firewall and a well-known pitfall

This is where many people trip up: Docker bypasses your ufw rules. If you write ports: - "8080:80" in the Compose file, Docker adds its own iptables rules that take effect before those of ufw. The service is then reachable from the internet even though ufw status does not list the port at all. This is not a bug but a deliberate design decision by Docker - it just keeps surprising people.

The clean way to deal with it:

  • Bind to localhost only: write "127.0.0.1:8080:80" instead of "8080:80". The container is then only reachable from the server itself.
  • Put a reverse proxy in front: nginx or Caddy listen on 80 and 443, terminate TLS and forward internally to the containers. Only these two ports are opened in the firewall.
  • Internal communication via the Compose network: containers in the same stack reach each other by their service name, so in the example above via db as host name. You do not need any port mapping for that.

How to set up the reverse proxy including a free SSL certificate is covered in the guide Set up a web server.

Keep disk space under control

Docker hoards: old images, orphaned volumes, build caches. On a VPS with limited NVMe storage, you notice that sooner than on a big machine. Get an overview regularly:

docker system df

Cleaning up works in two stages. The harmless variant removes stopped containers, unused networks and dangling images:

docker system prune

The thorough variant also takes unused volumes with it - and with them, potentially database contents:

docker system prune -a --volumes

Only run the second command if you know for sure which volumes are not in use right now. A backup beforehand never hurts.

Updates and operation

You update containers by pulling the image again and restarting the stack:

cd ~/stacks/demo
docker compose pull
docker compose up -d

Compose only replaces the containers whose image has actually changed. Data in named volumes is kept.

Two habits that pay off in permanent operation: use fixed version tags instead of latest, so that a pull does not bring an unwanted major upgrade. And back up your Compose files together with the volume contents - a compose.yaml on its own does not help you much after data loss.

A popular first real stack is your own cloud: the guide Install Nextcloud shows it step by step.

Frequently asked questions

Yes. Nesting is enabled on our VPS plans, so Docker Engine and Docker Compose run inside the container. You can pull images, build your own images and run several services in parallel via a Compose file. What does not work is nested virtualization, i.e. starting your own virtual machines via KVM inside the container.

A single small container - such as a Discord bot or a static web server - already runs on plan S. For a typical stack of reverse proxy, application and database, M is the sensible starting size. If you run several stacks in parallel or use memory-hungry applications such as Nextcloud with full-text search, choose L or XL. You can upgrade at any time without data loss.

Because Docker writes its own iptables rules that take effect before the ufw rules. This is known Docker behaviour and not a mistake in your configuration. The simplest solution: bind ports explicitly to localhost in the Compose file, i.e. "127.0.0.1:8080:80" instead of "8080:80", and put a reverse proxy in front as the only service reachable from outside.

Docker itself only a few hundred megabytes, the images considerably more. A slim Alpine image is around 10 MB, a complete application image can be several gigabytes. On top of that come build caches and orphaned layers. Check usage regularly with "docker system df" and clean up with "docker system prune".

Only if you specify it. Set "restart: unless-stopped" for each service in the Compose file - Docker then restarts the containers automatically after the VPS reboots. Without it, they stay stopped. Also check with "systemctl is-enabled docker" that the Docker service itself starts at boot.

Yes, Portainer runs as a normal container and works on the VPS. Do not bind its web interface unprotected to a public address, though. It is best to bind it to localhost only and reach it via an SSH tunnel or a secured reverse proxy.

A VPS for your Docker stack

A single small container already runs on plan S. For a typical stack of reverse proxy, application and database, M is the sensible starting size. All plans with NVMe SSD, unmetered traffic and full root access.

View VPS plans & prices