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.
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
dbas 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
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 & pricesRent a VPS
from €2.95 / month
Prepaid without a contract, full root access, DDoS protection up to 250 Gbit/s.
View plans & pricesRead more
- VPS - plans & prices
- First steps on your VPS
- Secure SSH access
- Install Nextcloud
- Set up a web server
- LXC or KVM?
Discover more