Containerization and virtualization on Linux: container vs VM comparison, Docker basics (image, container, volume, network), system containers with LXD, to full virtualization with KVM/QEMU along with real-world practice.

After episode 26 where we covered backup & restore strategy — the 3-2-1 principle, rsync, tar, restic, all the way to measured restore drills — you now have a solid data recovery foundation. But there's one fundamental question we haven't answered: what's the best way to run services on a single Linux server? Must every app have its own physical server? Must every service have its own OS? Or is there a lighter way?
In episode 27 we enter one of the most influential themes in how modern infrastructure works: containerization & virtualization. You'll learn three different levels of isolation — Docker for application containers, LXD for system containers, and KVM for full virtual machines — and understand when to use which. After this episode, the concepts we've built over 26 episodes (kernel, systemd, networking, storage, hardening) will look unified in a single, whole service architecture.
Imagine a boarding house with many residents. If all residents share the kitchen, bathroom, and electricity without boundaries, one noisy or wasteful resident can disturb everyone. Now imagine every room has its own door, its own electricity meter, and full facilities inside — each resident can manage their own way of living without disturbing others.
That's the essence of isolation. Before the container era, to run two application versions needing different libraries, you had to separate them onto two machines — a huge waste. Containers solve this by isolating an application and its dependencies within one shared kernel. Each container has its own filesystem, processes, and network namespace, but shares the host kernel.
The comparison between containers and virtual machines is the most important concept you must understand:
| Aspect | Container (Docker/LXC) | Virtual Machine (KVM) |
|---|---|---|
| Kernel | Shares the host kernel | Its own kernel per VM |
| Booting | Milliseconds (process only) | Seconds to minutes (full boot) |
| Overhead | Very low (MB) | High (GB, RAM & CPU allocation) |
| Isolation | Namespaces & cgroups | Full hardware virtualization |
| Image size | MB (shared layers) | GB (full disk image) |
| Density per host | Tens to hundreds | Units to dozens |
| Security boundary | Shared kernel (escape risk) | Strong hardware isolation |
| Use case | Microservices, CI/CD, stateless apps | Heterogeneous workloads, need different kernel/OS, legacy |
Note
The analogy most often used: containers are like container ships — all ships can carry containers of different sizes and contents, loaded and unloaded with the same equipment. Virtual machines are more like a cargo ship built specifically for certain cargo: heavier, more rigid, but more self-sufficient. Both have their place in logistics — and both have their place in real infrastructure.
Docker is the most popular container platform and is often people's entry point into containerization. There are five core concepts you must master: image, container, volume, network, and registry.
Let's start by running our first nginx container:
docker run -d --name web-nginx -p 8080:80 nginx:1.27Let's break down the command line by line:
-d — run in the background (detached).--name web-nginx — give a name for easy referencing.-p 8080:80 — port mapping: port 8080 on the host is forwarded to port 80 inside the container.nginx:1.27 — the image name and version tag. Docker will download this image from the registry if it's not already local.Verify the result with these commands:
docker ps
curl http://localhost:8080CONTAINER ID IMAGE COMMAND PORTS NAMES
a1b2c3d4e5f6 nginx:1.27 "/docker-entrypoint.…" 0.0.0.0:8080->80/tcp web-nginxImportant
Notice one thing that distinguishes containers from regular processes: containers are ephemeral. Everything written to a container's filesystem (not a volume) vanishes when the container is deleted. This is a feature, not a bug — applications are treated as stateless, and important data must be stored in volumes or an external database. Think of containers like plates at a restaurant: they can be washed and reused, but their contents (data) must be prepared in the kitchen (volume/database).
To reuse it without typing long commands each time, we define the service in a compose.yaml file. Docker Compose lets us describe multi-container setups — including volumes and networks — declaratively, so it can be committed to Git:
services:
web:
image: nginx:1.27
ports:
- "8080:80"
volumes:
- webdata:/usr/share/nginx/html
volumes:
webdata:docker compose up -d
docker compose psNAME IMAGE COMMAND STATUS PORTS
learn-linux-web-1 nginx:1.27 "/docker-entrypoint.…" Up 2 seconds 0.0.0.0:8080->80/tcpDocker isolates a single application. LXC (Linux Containers) and its modern manager LXD isolate an entire operating system — you run a "container" holding a full Linux distro (Ubuntu, Debian, Arch, even CentOS) with its own init/systemd, users, and processes. Because they share the host kernel, LXD is far lighter than a VM — but from the inside, it feels like having your own full Linux server.
The most accurate analogy: Docker is a toolshed — just a place for equipment; LXD is a prefabricated house — ready to live in with all the rooms, but still standing on the same land.
After installing lxd and running lxd init (or lxc init for older versions), you can launch an Ubuntu container in seconds:
lxc launch ubuntu:24.04 web-srv
lxc list
lxc exec web-srv -- apt update
lxc exec web-srv -- apt install -y nginx+---------+---------+---------------------+------+-----------+-----------+
| NAME | STATE | IPV4 | TYPE| SNAPSHOTS | LOCATION |
+---------+---------+---------------------+------+-----------+-----------+
| web-srv | RUNNING | 10.59.176.56 (eth0) | CONTAINER | 0 | (none) |
+---------+---------+---------------------+------+-----------+-----------+Notice that lxc exec web-srv -- apt ... runs commands inside the container — like ssh into a server, but without networking. This container is a full Ubuntu distro, has systemd, and can be reached from the network with its own IP. That's why LXD is often called a lightweight virtualization replacement: container density per host can reach tens even hundreds.
When you need your own kernel, an OS different from the host, or the strictest security isolation, there's no substitute for a virtual machine. KVM (Kernel-based Virtual Machine) leverages hardware virtualization (VT-x/AMD-V) so each VM runs its own guest kernel for real — not simulated. QEMU is the emulator acting as KVM's user-space front-end.
The most fundamental difference from containers: a VM shares nothing with the host besides its hardware. You can run Windows, FreeBSD, or old Linux distros on top of an Ubuntu host — something impossible with containers, since all containers must use the same kernel as the host.
Creating a VM from the CLI with virt-install:
virt-install \
--name vm-app1 \
--vcpus 2 \
--memory 2048 \
--disk size=20 \
--os-variant ubuntu24.04 \
--network network=default \
--cdrom /mnt/iso/ubuntu-24.04.isoOnce the VM is created, day-to-day management is done with virsh:
virsh list --all
virsh start vm-app1
virsh console vm-app1
virsh shutdown vm-app1
virsh destroy vm-app1 Id Name State
-------------------------------
3 vm-app1 running
- vm-backup offFor users who prefer a graphical interface, virt-manager provides a full GUI for creating and managing VMs — very useful for visual inspection, snapshots, and hardware settings. In server environments without a GUI, the combination of virt-install + virsh + the VM definition XML files is the main workflow.
Tip
First check whether your CPU supports hardware virtualization with lscpu | grep -i virtualization (look for the vmx flag for Intel or svm for AMD). If it doesn't appear, VMs can still run but via pure emulation — very slow. On cheap VPS/cloud that abuse nested virtualization, this is often the cause of VMs "hanging" without a clear reason.
Time to compare the two approaches directly on one machine. We'll run the same nginx twice: once as a Docker application container, once as an LXD system container.
Step 1 — Make sure both tools are available:
docker --version
lxc --versionStep 2 — Run nginx in Docker:
docker run -d --name web-nginx -p 8080:80 nginx:1.27
curl -s http://localhost:8080 | head -3Step 3 — Run nginx in LXD:
lxc launch ubuntu:24.04 web-srv
lxc exec web-srv -- apt-get update
lxc exec web-srv -- apt-get install -y nginx
lxc exec web-srv -- systemctl enable --now nginx
curl -s http://10.59.176.56/ | head -3Step 4 — Compare the resources used:
docker stats --no-stream web-nginx
lxc exec web-srv -- free -mCaution
Notice the difference in mental model: in Docker we run an application (docker run nginx), in LXD we run a full server then install the application inside it. The consequence: Docker containers are ready to die-anytime and be replaced (stateless), while LXD containers behave like a server that must be maintained — patched, monitored, and backed up like a regular server. Don't mix them up: treating LXD containers like Docker containers will make you forget to maintain them, and treating Docker containers like LXD will make data vanish without a trace.
Based on field experience, here are the error patterns most commonly seen when people start playing with containers and virtualization:
| Pitfall | Why It's Dangerous | Solution |
|---|---|---|
User not in the docker group | Constantly needs sudo; confuses the user | sudo usermod -aG docker $USER + logout/login |
| Container without a volume | Data lost when the container is deleted | Always define volumes for persistent data |
| Port mapping conflict | docker run -p 8080:80 fails because the port is taken | Check ss -tlnp, use a unique port per service |
Forgetting the -p port mapping | Container runs but can't be reached from outside | Remember: containers have their own IP; the host needs port mapping |
| Nested virtualization not supported | VM severely slows down / fails to boot | Check the vmx/svm flags; on VPS use containers only |
| Updating the host = forgetting containers/systemd | Base images already outdated with CVEs | Use pinned images + scheduled rebuild pipeline |
| No resource limits | One container can consume all host RAM | docker run --memory, --cpus, and cgroup limits in LXD |
One interesting case is users in the docker group. Adding a user to the docker group is equivalent to giving them root access to the machine — because anyone who can run docker run -v /:/host ... can read and write the entire host filesystem. This convenience must be paid for with great trust:
docker run hello-world # ERROR: permission denied
sudo usermod -aG docker $USER
newgrp docker # activate group membership without logout
docker run hello-world # successWarning
Never add a user to the docker group just for convenience in production. In serious environments, Docker access should be restricted via rootless docker or Docker context + TLS. Docker group members can privilege escalate to root at any time. If users have already been added to that group, check who has access — because that's equivalent to who has root access to the host.
In this episode we mapped out the three isolation layers available on Linux. Docker isolates applications lightly and efficiently — the top choice for microservices and stateless workloads. LXD isolates an entire operating system with low overhead — ideal for replacing lean VMs with full distros. KVM/QEMU provides full virtualization with its own guest kernel — irreplaceable for maximum security, heterogeneous OSes, and legacy workloads. You also practiced running nginx in both Docker and LXD, and understood the common costly mistakes.
With the ability to isolate and run many services on one server, you enter a new phase of Linux administration. But the sheer number of servers and users also raises a new problem: how do you manage user identities across many machines at once? In the next episode 28, we'll cover LDAP & centralized authentication — building a single source of truth for authentication so one account can log into dozens of servers. See you in episode 28!