LXC: Lightweight Virtualization as a Docker Alternative
AI generated
$
/etc
Linux · LXC · LXD · System Containers
LXC: Lightweight Virtualization as a Docker Alternative
Complete System Environments Instead of Individual Processes

LXC provides true system containers with a full userspace, its own init system and several parallel services, while Docker packages a single application process. For admins looking for a lightweight alternative to virtual machines, the LXD workflow, from installation through networking and storage to snapshots, offers a practical foundation for production environments where a fully-fledged operating system is needed rather than a single process.

16 min read LXC · LXD · System Containers · Namespaces Ubuntu 24.04 · Debian 12 · ZFS

1. What LXC Is and Why It Works Differently from Docker

LXC (Linux Containers) is the oldest production-ready container technology on Linux, dating back to 2008, long before Docker existed. LXC uses exactly the same kernel mechanisms as Docker: namespaces (PID, network, mount, UTS, IPC, user) and control groups (cgroups) for resource limiting. The fundamental difference isn't in the kernel technology but in the design goal. Docker was built to package and ship a single application in a reproducible way. LXC was built to lightly isolate a complete operating system userspace, the way you'd expect from a virtual machine, just without its own kernel and without a hypervisor.

In practice this means: an LXC container starts systemd (or another init system) as PID 1, runs multiple services inside it in parallel, has its own SSH daemon, its own cron jobs and its own log rotation, exactly like a physical or virtual server. A Docker container, by contrast, typically encapsulates exactly one process. When that process ends, the container ends. Anyone coming from the Docker world who starts an LXC container is often surprised how much it feels like an SSH login to a freshly installed server, rather than a docker exec into a minimal application container.

2. System Containers vs. Application Containers

The difference between system containers and application containers is most visible in the lifecycle and the image concept. A Docker image is a layered filesystem made up of the application, its runtime and its dependencies, built as an immutable artifact, versioned and deployed. Updates usually mean: build a new image, discard the old container, start a new one. An LXC or LXD image, on the other hand, is a complete root filesystem of a Linux distribution that you administer like an ordinary server installation: apt update, apt upgrade, editing config files, restarting services, all directly inside the running container.

Historically there are two flavors of LXC: classic LXC with its lxc-* command-line tools and text-based configuration files, and LXD, Canonical's more modern daemon, which uses LXC as a backend but adds a REST API, a unified lxc command line and clustering capabilities. For new projects, LXD is the standard choice today; classic LXC still shows up in older setups or when direct access to low-level configuration is required. The classic configuration file below shows how granularly individual namespaces and resource limits can be defined per container.


# /var/lib/lxc/webserver/config - classic LXC container config
lxc.uts.name = webserver

# Rootfs and template origin
lxc.rootfs.path = dir:/var/lib/lxc/webserver/rootfs

# Network: veth pair attached to the host bridge
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx

# Resource limits via cgroups
lxc.cgroup2.memory.max = 2G
lxc.cgroup2.cpu.max = 200000 100000

# Unprivileged container: map container UID/GID to unprivileged host range
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

# Apparmor confinement profile
lxc.apparmor.profile = generated

3. Installing and Initializing LXD

LXD is installed on most distributions via Snap or the native package repository; on Ubuntu it's preinstalled by default or available via apt install snapd && snap install lxd. After installation comes the mandatory setup step lxd init, an interactive wizard that asks about the storage backend, network bridge and remote API access. For production servers, ZFS or Btrfs is recommended as the storage backend, because both natively support copy-on-write snapshots and can create container clones in seconds rather than minutes, without fully copying the data.

The non-interactive mode lxd init --auto is suited to automation with Ansible or Terraform, but it then uses default values that you usually want to override explicitly in configuration management scripts. After initialization, LXD automatically creates the lxdbr0 bridge, through which new containers reach the network via NAT. A quick check with lxc network list and lxc storage list confirms that the network and storage pool were created correctly before the first container is launched.


#!/usr/bin/env bash
# Install LXD and run a non-interactive initialization
sudo apt update
sudo apt install -y snapd
sudo snap install lxd

# Non-interactive init: ZFS storage pool + default bridge network
sudo lxd init --auto \
  --storage-backend=zfs \
  --storage-pool=default \
  --storage-create-loop=20 \
  --network-address=auto \
  --network-port=8443

# Verify the resulting network and storage configuration
lxc network list
lxc storage list

4. Your First Container: lxc launch, exec, list

The lxc launch command loads an image and immediately starts a running container in a single step, which corresponds to the two separate Docker commands docker pull and docker run. Images are referenced via aliases such as ubuntu:24.04 or images:debian/12; LXD downloads them automatically from the configured image server when needed and caches them locally. lxc list then shows all containers with status, IPv4/IPv6 address and the image alias used in a clear table, comparable to docker ps, but with more network details visible directly.

To access a running container, use lxc exec, which, unlike docker exec, needs no special preparation of the application because a complete shell environment with bash, systemctl and the usual Linux tools is already present. lxc stop, lxc start and lxc delete manage the lifecycle, with lxc delete by default only deleting stopped containers, a sensible safeguard against accidentally deleting running systems. The --force flag can bypass this protection when needed.


# Launch a fresh Debian 12 system container
lxc launch images:debian/12 webserver

# List all containers with status, IP address and image alias
lxc list

# Open an interactive shell inside the running container
lxc exec webserver -- bash

# Inside the container: install and manage services like on a real server
apt update && apt install -y nginx
systemctl status nginx

# Lifecycle management from the host
lxc stop webserver
lxc start webserver
lxc delete webserver --force

5. Networking: Bridges, Profiles and Port Forwarding

Newly created containers automatically attach to the lxdbr0 bridge and receive a private IP address via DHCP, which is translated into the rest of the network via NAT, just like Docker's default bridge network. For containers that should appear on the local network like standalone physical machines, a macvlan or bridged network device is a good option, giving the container its own MAC address and an IP address from the same subnet as the host. This is especially useful when a container should get its own DHCP entry in the router or a fixed IP from the internal DNS zone.

Port forwarding from a host port to a container port works via a proxy device, which LXD manages directly without having to maintain iptables rules by hand. Profiles bundle recurring configuration such as network devices, resource limits and storage assignments and can be applied to multiple containers at once, comparable to a docker-compose service template, only reusable across containers. A container can reference multiple profiles at the same time, with later profiles overriding earlier settings.


# lxc profile edit webserver-profile
config:
  limits.cpu: "2"
  limits.memory: 2GB
description: Profile for public-facing web containers
devices:
  eth0:
    name: eth0
    network: lxdbr0
    type: nic
  http-proxy:
    # Forward host port 8080 to container port 80 without manual iptables
    connect: tcp:127.0.0.1:80
    listen: tcp:0.0.0.0:8080
    type: proxy
  root:
    path: /
    pool: default
    type: disk
name: webserver-profile

6. Storage Pools: ZFS, Btrfs and Copy-on-Write

LXD abstracts storage through pools that can be based on different backends: dir for the simple directory backend with no special features, plus ZFS, Btrfs and LVM for backends with native copy-on-write. The practical advantage of ZFS shows up when cloning: lxc copy creates a complete copy of an existing container in seconds, because only the metadata of the snapshot reference is copied, not the actual data blocks. Only once the clone changes data are the affected blocks actually duplicated, classic copy-on-write behavior.

For different workloads, multiple storage pools can run in parallel, for example a fast NVMe-based ZFS pool for database containers and a separate HDD pool for archive containers with rarely changing data. Storage volumes can also be created independently of individual containers and mounted as additional disk devices in several containers at once, useful for data directories shared between multiple services, without the detour through network shares like NFS.


# Create a dedicated ZFS pool for LXD on a spare block device
sudo zpool create tank-lxd /dev/sdb
sudo lxc storage create fast-zfs zfs source=tank-lxd/lxd

# Launch a container on the new pool
lxc launch images:debian/12 db-server --storage fast-zfs

# Clone via copy-on-write: near-instant, only metadata is duplicated
lxc copy db-server db-server-staging

# Create a shared data volume and attach it to two containers
lxc storage volume create fast-zfs shared-data
lxc storage volume attach fast-zfs shared-data app1 /mnt/shared
lxc storage volume attach fast-zfs shared-data app2 /mnt/shared

7. Snapshots, Backups and Live Migration

Snapshots in LXD are nearly instantaneous thanks to the storage backend's copy-on-write behavior. lxc snapshot creates a consistent state of the entire root filesystem, and lxc restore resets the container exactly to that state, which is excellent for risky updates, for example before a major distribution upgrade or a configuration change that might turn out to be faulty. Docker has no equivalent concept in this form, because containers there are considered stateless and persistence is handled via external volumes instead.

For migrating between hosts, LXD offers lxc copy targeting a remote server, which transfers the entire container including all snapshots over the network, optionally even live while the container keeps running. lxc export additionally produces a portable backup archive in tar format that can be re-imported on another system independently of LXD. For regular backups, lxc snapshot is combined with a cron job and a retention policy that automatically rotates old snapshots.

8. Security: Unprivileged Containers and Isolation

LXD starts containers as unprivileged by default, meaning root inside the container is mapped via a user namespace to an unprivileged UID range on the host. A process running as UID 0 inside the container actually has a high, non-privileged UID like 100000 on the host, and even in the event of a container escape via a kernel vulnerability, the damage on the host remains tightly limited. This default differs significantly from older Docker setups, where root inside the container is often also root on the host, unless user namespace remapping has been explicitly configured.

AppArmor profiles and seccomp filters additionally isolate every container by default, blocking dangerous syscalls such as mounting host filesystems or loading kernel modules from inside the container. For workloads that genuinely need privileged access, such as nested containers or certain VPN setups, a container can be deliberately marked as privileged, which significantly reduces the isolation guarantees and should only be considered for trusted workloads. The rule of thumb stays: unprivileged by default, privileged only with explicit justification.

9. Use Cases and Comparison with Docker

LXC is particularly well suited to scenarios where a complete, VM-like system state is needed, but the overhead of a real virtual machine with its own kernel is undesirable. Typical use cases are test environments that should look exactly like a production server with multiple services, cron jobs and its own systemd instance, isolated development environments per customer or project, and recreating entire legacy server landscapes on a single physical machine for training or migration testing.

LXC also has a firm place as a lightweight replacement for classic VMs on Hetzner, Proxmox or self-managed KVM hosts, whenever several isolated Linux systems need to run on one host without paying the memory and CPU overhead of a hypervisor and a separate kernel for each system. Docker, on the other hand, remains the right choice for stateless, horizontally scalable application containers in CI/CD pipelines and microservice architectures, where reproducibility and fast rebuilding matter more than a complete system state.


{
  "name": "webserver",
  "status": "Running",
  "type": "container",
  "architecture": "x86_64",
  "config": {
    "security.privileged": "false",
    "security.nesting": "false",
    "limits.cpu": "2",
    "limits.memory": "2GB"
  },
  "state": {
    "network": {
      "eth0": {
        "addresses": [
          { "family": "inet", "address": "10.98.14.22", "netmask": "24", "scope": "global" }
        ]
      }
    },
    "pid": 48213
  }
}
Use Case Docker (Application Container) LXC (System Container) Deciding Factor
Test environment with multiple services needs multiple containers + Compose a single container is enough Less orchestration overhead
Recreating an existing VM 1:1 requires a Dockerfile rebuild behaves like a real server No application refactoring needed
Stateless microservices in CI/CD fast, reproducibly replaceable system containers too heavy Reproducibility matters more than state
Multiple isolated Linux systems instead of VMs no own init/systemd full systemd instance per container VM-like behavior without a hypervisor
Horizontal scaling in Kubernetes native container runtime target no native Kubernetes backend Ecosystem compatibility

In practice, the two technologies aren't mutually exclusive: many teams run LXC system containers for long-lived infrastructure services such as databases, VPN gateways or internal development servers, while the same infrastructure uses Docker in parallel for stateless application deployments. Ultimately, the decision comes down to whether a single process or a complete system state needs to be isolated.

Mironsoft

Container infrastructure, system virtualization and server automation

Ready to set up production-grade LXC system containers?

We build LXD infrastructure with ZFS storage, secure network setup and automated snapshots, migrate existing VMs into system containers without data loss, and set up unprivileged, hardened environments for production workloads.

LXD Infrastructure Setup

ZFS storage pools, network bridges and cluster configuration across multiple hosts

VM-to-Container Migration

Recreate existing virtual machines as system containers without rebuilding applications

Security Hardening

Unprivileged containers, AppArmor profiles and snapshot backup strategies

10. Summary

LXC solves a different problem than Docker, even though both build on the same kernel mechanisms. Where Docker reproducibly packages a single application process, LXC isolates a complete system state with its own init system, several parallel services, and an administration experience that feels like a real server. The LXD workflow, from installation through lxc launch and network profiles to ZFS-based snapshots, provides a lightweight, production-ready foundation for that, without the overhead of a full hypervisor.

For test environments, legacy replicas and replacing classic VMs, LXC remains the more pragmatic choice, while Docker plays to its strengths in stateless, horizontally scalable microservice architectures. Anyone who deliberately deploys both technologies based on the use case, rather than committing to a single container philosophy, gets the benefits of each without unnecessary compromises on isolation, operational effort or resource usage.

LXC as a Docker Alternative: The Essentials at a Glance

System vs. Application Containers

LXC isolates a complete operating system with its own init system, Docker packages exactly one application process.

LXD Workflow

lxd init, lxc launch, lxc exec and lxc list are enough to cover the entire container lifecycle.

Storage & Snapshots

ZFS or Btrfs deliver copy-on-write clones in seconds and near-instant lxc snapshot backups.

Security

Unprivileged containers with user namespace remapping are the secure default, privileged only with justification.

11. FAQ: LXC as a Docker Alternative

1What is the difference between LXC and Docker?
Docker usually packages a single process in an immutable image. LXC isolates a complete operating system userspace with its own init system and several parallel services.
2Is LXC the same as a virtual machine?
No. LXC containers share the host kernel via namespaces and cgroups, while a VM brings its own kernel. LXC is lighter, but isolates more weakly than a VM.
3What is the difference between LXC and LXD?
LXC is the kernel container technology. LXD is Canonical's more modern daemon with a REST API, a unified command line and clustering features.
4Can I run Docker containers inside LXC?
Yes, with the security.nesting option enabled, though this requires extra configuration effort and slightly reduces isolation compared to Docker running directly on the host.
5Are LXC containers secure enough for production systems?
Yes, with unprivileged containers, user namespace remapping, AppArmor and seccomp, all active by default in LXD. Privileged containers only with explicit justification.
6Which storage backends does LXD support?
Among others, dir, ZFS, Btrfs, LVM and Ceph. For production, ZFS or Btrfs are recommended thanks to native copy-on-write for fast clones and snapshots.
7How do I access an LXC container from outside?
Via a proxy device for port forwarding, via a macvlan/bridged device for a dedicated LAN IP, or directly with lxc exec from the host.
8Can I migrate an LXC container between servers?
Yes, with lxc copy to a remote server, optionally live. lxc export additionally produces a portable backup archive independent of LXD.
9Does systemd run inside an LXC container?
Yes, systemd runs by default as PID 1 and manages services, cron jobs and log rotation just like on a physical or virtual server.
10When should I use LXC instead of Docker?
For a complete system state with multiple services, such as test environments, legacy replicas or as a lightweight VM replacement. For stateless apps, Docker remains the better fit.