1. Overview

Linux Containers (LXC) is a toolset in Linux that creates an isolated environment in a single host. Specifically, the LXC creates environments isolated from each other regarding computing resources and root filesystems using the Linux kernel’s namespaces and cgroups function.

One distinction between the LXC and the virtual machines (VMs) is that different LXC instance shares the same operating system (OS) kernel. On the other hand, the VMs share only the physical hardware they each run their own OS stack, including the kernel.

In this tutorial, we’ll look at the difference between the LXC and Docker containers and various commands from the LXC toolset for managing the containers.

2. Difference Between LXC and Docker Containers

The LXC and Docker containers are both OS-level virtualizations with similar implementations. Specifically, both utilize Linux’s cgroups and namespaces to achieve OS-level isolations.

We can consider the distinction between the two different container solutions from how they are used. Specifically, the Docker container is designed to run a single process per container. This paradigm is also commonly known as application-level isolation. On the other hand, the LXC is meant for running a full-fledged OS in isolation.

The community typically uses LXC as a lightweight alternative to the VM where the container is created as a base from which modification is made on top of it. In contrast, the Docker container is not intended to be modified after it has been created.

For a comprehensive walkthrough of the differences between the two, check out the article that tells the history of the Docker container and how it relates to the LXC.

3. Getting Started With LXC

The LXC toolset contains various commands for managing the containers. For instance, the lxc-createlxc-startlxc-stop, and lxc-destroy commands are all commands that are in the LXC toolset for managing the containers. In the following sections, we’ll learn how to create our first LXC container and manage it using all those commands.

But first, we’ll need to obtain the LXC toolset.

3.1. Installing the LXC Toolset

The LXC lists various requirements on their site that our host system has to meet for LXC to function properly. For example, the LXC requires that one of the glibc, musl libcuclib, or bioninc C libraries must be present in the system. Additionally, the Linux kernel of the host must be 2.6.32 or above.

Fortunately, most recent Linux distributions have already included those dependencies for LXC installation to be a simple process. Concretely, we can run the apt-get install lxc command to install the LXC toolset in Ubuntu Linux:

$ sudo apt-get install -y lxc

We can use the lxc-checkconfig command to verify that our kernel configuration can support the LXC:

$ lxc-checkconfig
LXC version 4.0.12
Kernel configuration not found at /proc/config.gz; searching...
Kernel configuration found at /boot/config-5.15.0-92-generic
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
...

Importantly, we should have the entire Namespaces and Control Groups enabled to ensure the LXC toolset works as expected.

3.2. Template

In LXC, a template refers to a set of scripts that initialize a container’s root filesystem (rootfs). This initialization process involves installing a basic set of command-line tools and libraries essential for the container’s operation. For instance, when creating a Debian container, the Debian template includes a script that downloads and installs the Debian-specific toolset. This helps ensure that the container contains the necessary utilities and environment to function as a minimal Debian system.

To view a list of available templates in our system, we can browse the /usr/share/lxc/templates directory using the ls command:

$ ls /usr/share/lxc/templates/
lxc-alpine    lxc-archlinux  lxc-centos  lxc-debian    lxc-fedora         lxc-gentoo  lxc-oci           lxc-opensuse  lxc-plamo  lxc-sabayon    lxc-sparclinux  lxc-ubuntu        lxc-voidlinux
lxc-altlinux  lxc-busybox    lxc-cirros  lxc-download  lxc-fedora-legacy  lxc-local   lxc-openmandriva  lxc-oracle    lxc-pld    lxc-slackware  lxc-sshd        lxc-ubuntu-cloud

As their name implies, the different scripts create Linux containers for different Linux distributions. For example, the lxc-archlinux creates a container that replicates the Arch Linux distribution.

Notably, the lxc-download is a special template that allows us to connect to download templates during the container creation. It pulls the image index from the LXC repository and downloads the desired templates for creating the container. Compared to the static template in our system, using the lxc-download template provides access to the latest template scripts. Hence, we should prefer the lxc-download template most of the time.

3.2. Creating a Container

To create a container, we can use the lxc-create command followed by the name of the template from which we’ll base our container:

$ sudo lxc-create --name alpine-container --template download
Downloading the image index

---
DIST    RELEASE    ARCH    VARIANT    BUILD
---
almalinux    8    amd64    default    20240601_23:08
almalinux    8    arm64    default    20240601_23:08
almalinux    9    amd64    default    20240601_23:08
...
---

Distribution:

In the command above, we use the download template to create our container and we name the container as alpine-container.

If we know the exact template we want to use, we can specify it on the lxc-create command to prevent the interactive input prompt:

$ sudo lxc-create --name alpine-container --template download -- --dist alpine --release 3.20 --arch amd64
Using image from local cache
Unpacking the rootfs

---
You just created an Alpinelinux 3.20 x86_64 (20240601_13:00) container.

The command above specifies the Alpine distribution of release version 3.20 and the amd64 architecture. When the command executes successfully, we’ll have created our first container.

The container we’ve created is essentially a directory under /var/lib/lxc:

$ sudo ls /var/lib/lxc
alpine-container

In the directory we’ll see the rootfs and the config for the container:

$ sudo ls /var/lib/lxc/alpine-container
config    rootfs

The config file along with the rootfs directory is what defines a container.

3.3. Starting and Checking Container’s Status

Creating a container doesn’t automatically start it. To run the container, we can use the lxc-start command:

$ sudo lxc-start --name alpine-container

The command starts the container and returns an exit code of 0 on success. At this point, we’ll have a running container in our system.

Then, we can inspect the status of our container using the lxc-info command:

$ sudo lxc-info --name alpine-container
Name:           alpine-container
State:          RUNNING
PID:            12021
IP:             10.0.3.62
CPU use:        0.23 seconds
BlkIO use:      4.00 KiB
Memory use:     1.43 MiB
KMem use:       908.00 KiB
Link:           vethmI1a7M
 TX bytes:      1.83 KiB
 RX bytes:      4.47 KiB
 Total bytes:   6.30 KiB

The lxc-info prints the status and usage info of a container by its name. This information includes the state of the container, the PID, and the IP address of the container from the host’s perspective. Then, we can also get the container’s CPU, IO, memory, and network usage information from the output.

3.4. Attaching to a Container

We can attach to a running container using the lxc-attach command:

$ sudo lxc-attach --name alpine-container
/ # 

The command starts a shell in the running container and connects our STDIN, STDOUT, and STDERR to the container’s shell.

We can verify that the shell is indeed in the container by checking the os-release file:

/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.20.0
PRETTY_NAME="Alpine Linux v3.20"
...

To exit the container’s shell, we can invoke the exit command:

/ # exit

3.5. Stopping and Destroying a Container

To stop a running container, we can use the lxc-stop followed by the name of the container:

$ sudo lxc-stop --name alpine-container

If the command exits with status code 0 without any error message, the container is successfully terminated. We can verify the state of the container using the lxc-info command:

$ sudo lxc-info --name alpine-container
Name:           alpine-container
State:          STOPPED

As we can see, the container is indeed in the STOPPED state.

To clean up a container, we can use the lxc-destroy command to remove the container entirely from the host:

$ sudo lxc-destroy --name alpine-container

We can verify that the container has indeed been removed by checking the /var/lib/lxc directory:

$ sudo ls -halt /var/lib/lxc
total 8.0K
drwx------  2 root root 4.0K Jun  2 10:11 .
drwxr-xr-x 75 root root 4.0K Jun  2 07:12 ..

4. Conclusion

In this tutorial, we’ve learned that the LXC is a userspace toolset that utilizes the kernel feature cgroups and namespaces to create an isolated process known as containers. Then, we looked at two important directories in the context of LXC. Firstly, the /usr/share/lxc/templates are the directory for storing the LXC templates. The /var/lib/lxc directory that stores the container files.

Subsequently, we’ve also explored several basic commands that allow us to manage the container lifecycle. This includes the lxc-create and lxc-start for creating and starting a container. Then, the lxc-info is the command for getting the container’s info and state. Finally, the lxc-stop command stops a running container, and the lxc-destroy command removes a container from the host.