Introduction to LXD

Introduction to LXD

Bitnesia Aug 28, 2026 1 ID

Chapter 28 closed the discussion on full virtualization with a fairly clear consequence: every KVM VM carries its own kernel, complete with all the boot overhead and resource consumption that comes with it. Docker containers in Chapters 26 and 27 are on the opposite end, extremely lightweight because they share the kernel with the host, but designed to run a single application process and terminate once that process completes. Sysadmins in the field often face requirements that do not fit neatly into either end. What if developers need an environment that feels like a full Ubuntu server, complete with systemd, multiple services running simultaneously, and accessible via SSH like a regular VM, but without having to bear the boot overhead and RAM allocation of KVM? Chapter 29 addresses this need through system containers, practiced using LXD, an official container and virtual machine manager created by Canonical that positions itself right in the middle between Docker and KVM. This chapter starts with the concepts of system containers and the history of LXD, followed by installation and initialization on Ubuntu Server 26.04 LTS, hands-on creation and management of your first system container, and concludes with a guide on choosing between LXD and Docker based on real-world requirements.

29.1 System Container Concepts: LXC and LXD

Before diving into the installation, we need to understand where LXD originates and what sets its approach apart from Docker, which we have mastered, and KVM, which we just learned.

29.1.1 LXC as the Foundation, LXD as the Management Layer

LXC (Linux Containers) is a low-level userspace interface that leverages the Linux kernel's namespaces and cgroups, the exact same isolation technologies underlying Docker. However, LXC arrived earlier around 2008, well before Docker became popular. Managing containers directly through LXC was quite cumbersome due to the lack of high-level management tooling, prompting Canonical to develop LXD in 2014 as a daemon and REST API built on top of LXC, adding image management, storage pools, networking, and snapshots that are much easier to use via a single client binary named lxc.

One common source of confusion to clear up from the start: the name lxc in LXD is not the LXC described above. LXD actually uses the name lxc for its own client binary, completely separate from legacy LXC commands like lxc-create, lxc-start, and similar utilities. Throughout this chapter, every lxc ... command refers to the LXD client, not the low-level LXC tooling that is rarely used directly in production anymore.

29.1.2 The 2023 Fork: Incus as a Community Alternative

In August 2023, Canonical began requiring all LXD contributors to sign a Contributor License Agreement (CLA) transferring full rights over those contributions to Canonical, while also restricting core LXD team membership exclusively to Canonical employees. This policy prompted several former LXD core team members, including Stéphane Graber, to fork LXD into a separate project called Incus under the Linux Containers organization, developed independently as a fully community-managed alternative.

This series consistently uses LXD rather than Incus, as LXD remains the official product fully supported by Canonical and tightly integrated with the Ubuntu ecosystem, from the snap LTS channels discussed in Section 29.2 to Ubuntu Pro support. For those interested in exploring community-driven alternatives, Incus is a worthy candidate for further study outside the scope of this chapter, given that both projects share the same codebase roots despite currently evolving in their own technical directions.

29.1.3 System Container vs Application Container vs Virtual Machine

The term system container refers to a container that simulates a full operating system, complete with an init system, process manager, and multiple processes running concurrently, offering a VM-like experience while still sharing the kernel with the host. This differs from an application container like those produced by Docker, which packages a single application process and is designed to stop once that process finishes. The two approaches are complementary rather than strictly competitive. Running Docker inside an LXD system container is a common pattern, whereas running LXD inside a Docker container is not a supported pattern.

AspectSystem Container (LXD)Application Container (Docker)Virtual Machine (KVM)
KernelShares host kernelShares host kernelDedicated kernel, fully isolated
Init systemFull systemd running insideGenerally none, single main processFull systemd, same as physical host
LifecycleLong-running like a server, manually managedStops once the main process completesLong-running like a physical server
Boot timeA few secondsNear-instant (milliseconds)Tens of seconds to minutes
Security isolationNamespaces and cgroups, typically unprivilegedNamespaces and cgroupsHardware-level isolation via CPU virtualization extensions

A key security detail worth noting upfront, as preparation for Part VIII on hardening: LXD containers run unprivileged by default, meaning the UID/GID range inside the container is mapped to a non-overlapping UID/GID range on the host via /etc/subuid and /etc/subgid. The root account inside the container is not actual root on the host, so a process that manages to break out of container isolation remains trapped as an unprivileged user on the host side. This adds an extra layer of defense that legacy privileged container models lacked.

LXD is actually more than just a system container manager. The same tool can also run full virtual machines using QEMU, the exact emulation engine we covered in Section 28.1.3. However, LXD manages the VM lifecycle directly through its own API without involving libvirt at all. Because LXD is distributed as a snap with strict confinement, the required QEMU binaries are delivered via connected snap dependencies rather than standard apt package dependencies used in pure KVM setups in Chapter 28. These VM capabilities are briefly highlighted in Section 29.3, although the primary focus of this chapter remains on system containers.

29.2 Installing and Initializing LXD on Ubuntu Server 26.04 LTS

This section covers installing LXD via snap, setting up non-root access, and initializing the baseline storage and networking required before creating your first container.

29.2.1 Installing LXD via Snap

Unlike Incus, which was briefly mentioned in Section 29.1.2 and is available as a native apt package, LXD was removed by Canonical from standard apt repositories long ago and is distributed exclusively via snap, Canonical's own containerized package mechanism that simplifies releases across Ubuntu versions without being tied to the LTS release cycle.

Practical Steps

  1. Ubuntu Server 26.04 LTS includes a small helper package named lxd-installer by default, acting as a wrapper that triggers the LXD snap installation when needed for the first time. If it is missing (such as in a minimal installation), install it first.
    sudo apt install lxd-installer
  2. Run the lxc command for the first time. lxd-installer will detect that the LXD snap is not yet installed and prompt for a channel selection (for example, latest/stable or 5.21/stable) before automatically executing the snap installation.
    lxc list
  3. As a more explicit alternative recommended for production servers to avoid interactive wizard steps, install the LXD snap directly with your target channel.
    sudo snap install lxd --channel=latest/stable

Verification and Troubleshooting

  • Confirm the installed snap and LXD version.
    snap list lxd
    lxd version
  • The latest/stable channel currently tracks the LXD 5.21 series as the LTS release recommended by Canonical for production workloads. The 6.x series (currently at 6.9) represents feature releases carrying newer functionality but is explicitly not recommended by Canonical for production environments, so stick to the default channel unless specific 6.x features are required and risks are accepted.
  • If the lxc or lxd commands are not recognized after installing the snap, open a new shell session or run hash -r, as the binary PATH for snaps (/snap/bin) is sometimes only fully registered in subsequent shell sessions.

29.2.2 Non-root Access via the lxd Group

Similar to the non-root access pattern for libvirt in Section 28.2.3, LXD uses a dedicated group named lxd so daily operations do not require prefixing commands with sudo.

Practical Steps

  1. Add your active user to the lxd group.
    sudo usermod -aG lxd $USER
  2. Apply the new group membership to the active shell session without performing a full logout.
    newgrp lxd

Verification and Troubleshooting

  • Verify that the lxc command runs without sudo.
    lxc list
  • The newgrp command only updates the currently active terminal session. Other open SSH sessions or future logins will only recognize the new group membership after logging out and logging back in.

29.2.3 Storage and Network Initialization via lxd init

LXD, much like libvirt in Section 28.2.4, requires a storage pool for instance disks and a network bridge for connectivity. LXD consolidates both initial configurations into a single interactive wizard called lxd init.

Practical Steps

  1. Launch the initialization wizard.
    lxd init
  2. Follow the interactive prompts. Below is a standard sequence of questions and recommended answers for a single-server setup in a lab environment (note that exact phrasing may vary slightly depending on the installed LXD version).
    Would you like to use clustering? (yes/no) [default=no]: no
    Do you want to configure a new storage pool? (yes/no) [default=yes]: yes
    Name of the new storage pool [default=default]: default
    Name of the storage backend to use (dir, lvm, zfs, btrfs, ceph) [default=zfs]: zfs
    Create a new ZFS pool? (yes/no) [default=yes]: yes
    Would you like to use an existing empty block device? (yes/no) [default=no]: no
    Size in GiB of the new loop device (1GiB minimum) [default=30GiB]: 30GiB
    Would you like to create a new local network bridge? (yes/no) [default=yes]: yes
    What should the new bridge be called? [default=lxdbr0]: lxdbr0
    What IPv4 address should be used? (CIDR subnet notation, "auto" or "none") [default=auto]: auto
    What IPv6 address should be used? (CIDR subnet notation, "auto" or "none") [default=auto]: auto
    Would you like the server to be available over the network? (yes/no) [default=no]: no
    Would you like stale cached images to be updated automatically? (yes/no) [default=yes]: yes
    Would you like a YAML "init" preseed to be printed? (yes/no) [default=no]: no

Several choices above warrant further explanation. The zfs storage backend is the default because it supports drastically faster snapshots and clones compared to the dir backend, which writes regular files to the filesystem, mirroring our rationale for selecting LVM over files for KVM in Chapter 7. If the zfsutils-linux package is not yet installed, LXD will offer to install it automatically via apt, so manual installation beforehand is unnecessary. The resulting lxdbr0 bridge functions identically to virbr0 in Section 28.2.4, executing dnsmasq for internal DHCP and DNS, then forwarding outbound traffic via NAT.

Verification and Troubleshooting

  • Inspect the newly created storage pool and network.
    lxc storage list
    lxc network list
  • Confirm that the lxdbr0 bridge has obtained an IP address on the host side.
    ip addr show lxdbr0
  • For quick lab environments or experiments that do not require optimal snapshot performance, running lxd init --minimal bypasses interactive prompts, instantly generating a dir storage pool and a NAT bridge with DHCP. The downside is that the dir backend does not support fast snapshots or cloning, making it unsuitable for the snapshot exercises in Section 29.3.5.
  • If the wizard has already been run and needs to be re-executed, running lxd init again will present prompts regarding existing configurations rather than overwriting them silently.

29.3 Creating and Managing System Containers with LXD

With storage and networking ready, this section walks through the complete lifecycle of a system container, from initial launch to snapshots for quick rollbacks.

29.3.1 Launching your First Container with lxc launch

LXD fetches operating system images from default remote image servers. The remote named ubuntu: points to official Ubuntu images released directly by Canonical, while the images: remote provides community-maintained images for various non-Ubuntu Linux distributions, managed by the Linux Containers project.

Practical Steps

  1. Launch a new container based on the official Ubuntu 26.04 image.
    lxc launch ubuntu:26.04 web01
  2. List running instances to see the auto-assigned IP address from lxdbr0.
    lxc list

Verification and Troubleshooting

  • A STATE column showing RUNNING alongside an IPv4 address in the IPV4 column indicates that the container is up and has received a DHCP lease from lxdbr0.
  • The lxc launch command automatically downloads the image if it is not available locally, making the initial launch take longer than subsequent launches that use the local image cache.
  • A notable difference from virt-install in Section 28.3.2 is that containers start within seconds without going through the Subiquity installation wizard, because the downloaded image is a pre-built Ubuntu root filesystem ready for immediate use, not an ISO installer.

29.3.2 Accessing Container Shells with lxc exec

Practical Steps

  1. Open an interactive shell inside the container.
    lxc exec web01 -- bash
  2. Run a single command without entering interactive mode, useful for automation scripts.
    lxc exec web01 -- apt update
    lxc exec web01 -- apt install -y nginx

Verification and Troubleshooting

  • A changed shell prompt indicates that your session is inside the container, not on the host. Run hostname to verify.
  • The double dash -- in lxc exec web01 -- bash separates flags meant for lxc exec itself from the command executed inside the container, preventing flags like -y in apt install from being misinterpreted as lxc exec options.
  • LXD also provides the shorthand alias lxc shell web01, equivalent to lxc exec web01 -- su -l, which drops directly into a root login session with proper environment variables set, offering a cleaner workflow than launching raw bash for routine interactive work.

29.3.3 Container Lifecycle Controls

CommandFunction
lxc listDisplay all instances and their current status
lxc stop web01Shut down a container gracefully
lxc start web01Start a stopped container
lxc restart web01Restart a container
lxc delete web01Permanently remove a container and its disk
lxc delete web01 --forceForce-delete a container while it is still running

Practical Steps

  1. Stop the container gracefully and check its status.
    lxc stop web01
    lxc list
  2. Start it again.
    lxc start web01

Verification and Troubleshooting

  • An important safety note on operational risks: lxc delete without --force will refuse to remove a running container as a safeguard. Once the --force flag is supplied, deletion is immediate and permanently wipes the container's disk data from the storage pool without extra confirmation steps or a recycle bin.
  • Containers failing to shut down gracefully within a reasonable timeframe (for example, due to internal zombie processes) can be forcibly stopped using lxc stop web01 --force, equivalent to pulling a power cable, carrying the same unwritten data loss risks as virsh destroy in Section 28.4.1.

29.3.4 Restricting Container Resources

Without explicit limits, a container can theoretically consume all available host CPU and RAM resources. Defining resource caps upfront prevents a single misbehaving container from starving other instances co-located on the same host.

Practical Steps

  1. Cap an existing container to a maximum of 1 vCPU and 512 MiB of RAM.
    lxc config set web01 limits.cpu=1
    lxc config set web01 limits.memory=512MiB
    lxc restart web01
  2. Alternatively, set resource limits directly when launching a new container.
    lxc launch ubuntu:26.04 web02 --config limits.cpu=1 --config limits.memory=512MiB

Verification and Troubleshooting

  • Display active resource limits.
    lxc config show web01
  • Verify from inside the container that the kernel correctly reports memory constraints, rather than just seeing it recorded in LXD configuration files.
    lxc exec web01 -- free -m
  • These limits are enforced using host-side cgroups v2, which is the exact same kernel mechanism underlying resource limits in Docker (Chapter 26) and cgroup v2 migrations in Section 5.5, rather than a custom LXD implementation.

29.3.5 File Transfer and Snapshots

Beyond lxc exec, LXD allows moving files directly between host and container without requiring SSH or shared folders, alongside snapshots for quick rollbacks prior to risky changes, similar to LVM snapshots in Section 7.2 and QEMU snapshots in Section 28.4.4.

Practical Steps

  1. Copy a host file into a container.
    lxc file push /etc/hosts web01/tmp/hosts-host
  2. Fetch a file from a container to the host.
    lxc file pull web01/etc/nginx/nginx.conf .
  3. Create a snapshot before applying risky modifications, such as package updates.
    lxc snapshot web01 pre-upgrade
  4. If changes cause issues, restore the container to its snapshot state.
    lxc restore web01 pre-upgrade
  5. Delete unneeded snapshots using the instance_name/snapshot_name format.
    lxc delete web01/pre-upgrade

Verification and Troubleshooting

  • List stored snapshots for a specific container.
    lxc list web01
  • Snapshots are fast and space-efficient only when using zfs or btrfs storage backends due to native copy-on-write capabilities. The dir backend used via --minimal in Section 29.2.3 still supports snapshots, but creates them by duplicating the full disk contents each time, making it significantly slower and storage-intensive for large containers.
  • Snapshots reside within the same storage pool as the target container, matching the notes on QEMU snapshots in Section 28.4.4. A physical host disk failure will still wipe out both the container and its snapshots simultaneously, meaning snapshots are not a replacement for off-site backups covered in Chapter 40.

29.4 Choosing System Containers (LXD) vs Docker

After working with Docker in Chapters 26 and 27 and LXD in this chapter, the most frequent production question is deciding when to deploy each technology, given that both share the host kernel and remain far lighter than full KVM VMs. The choice depends on workload architecture rather than simple tool preference.

CriterionChoose LXD (System Container)Choose Docker (Application Container)
Workload patternMulti-process environments resembling full servers (such as running web servers, databases, and cron services together)Single application process per container, following the single responsibility principle
LifecycleLong-running like a VM, managed like a mini serverEphemeral, recreated from base images during deployment
Administration workflowSimilar to standard VM management: SSH or lxc exec/lxc shell, patching packages via apt like physical machinesImmutable images where updates require rebuilding image layers rather than patching running containers
Orchestration ecosystemLimited, primarily geared toward single-host deployments or small clustersExtensive: Docker Compose (Chapter 27), Kubernetes, and vast public registry ecosystems
Common use casesMigrating legacy workloads from physical/KVM setups without major refactoring, multi-tenant VPS environments, isolated student/client labsModern cloud-native applications, microservices, CI/CD automation pipelines

From a operational perspective, using both technologies together is common in production infrastructure. Systems administrators providing VPS-style hosting often use LXD as an isolation layer between tenants, while developers inside those LXD containers remain free to run Docker Compose setups as demonstrated in Chapter 27. The underlying kernel supports nested containers as long as necessary security permissions (such as overlayfs) are granted by the parent container profile. Conversely, installing LXD inside a Docker container is neither a supported nor recommended pattern.

At this point, we have covered LXD's role as Canonical's official system container solution, completed snap installation, configured storage and networking setups on Ubuntu Server 26.04 LTS, and executed complete lifecycle, resource limit, and snapshot management workflows using the lxc CLI. This concludes Part VII on virtualization and containerization, covering three distinct approaches: Docker for ephemeral application isolation, LXD for lightweight VM-like system containers, and KVM for full hardware-level kernel isolation. Part VIII transitions into server security, beginning with Chapter 30 on system hardening, where minimal attack surface principles, including the unprivileged container architecture introduced in Section 29.1.3, will be explored systematically.