Requirements

Hard dependencies:

  • One of glibc, musl libc, uclib or bionic as your C library
  • Linux kernel >= 2.6.32

Extra dependencies for lxc-attach:

  • Linux kernel >= 3.8

Extra dependencies for unprivileged containers:

  • libpam-cgfs configuring your system for unprivileged CGroups operation
  • A recent version of shadow including newuidmap and newgidmap
  • Linux kernel >= 3.12

Recommended libraries:

  • libcap (to allow for capability drops)
  • libapparmor (to set a different apparmor profile for the container)
  • libselinux (to set a different selinux context for the container)
  • libseccomp (to set a seccomp policy for the container)
  • libgnutls (for various checksumming)
  • liblua (for the LUA binding)
  • python3-dev (for the python3 binding)

Installation

In most cases, you'll find recent versions of LXC available for your Linux distribution. Either directly in the distribution's package repository or through some backport channel.

For your first LXC experience, we recommend you use a recent supported release, such as a recent bugfix release of LXC 4.0.

If using Ubuntu, we recommend you use Ubuntu 18.04 LTS as your container host. LXC bugfix releases are available directly in the distribution package repository shortly after release and those offer a clean (unpatched) upstream experience.

Ubuntu is also one of the few (if not only) Linux distributions to come by default with everything that's needed for safe, unprivileged LXC containers.

On such an Ubuntu system, installing LXC is as simple as:

sudo apt-get install lxc

Your system will then have all the LXC commands available, all its templates as well as the python3 binding should you want to script LXC.

Use the following command to check whether the Linux kernel has the required configuration:

lxc-checkconfig

Create Privileged Containers

Privileged containers are containers that are created by root and run as root.

Privileged containers are the easiest way to get started learning about and experimenting with LXC, but they may not be appropriate for production use. Depending on the host Linux distribution, privileged containers may be protected by some capability dropping, apparmor profiles, selinux context or seccomp policies but ultimately, the processes still run as root and so you should never give access to root inside a privileged container to an untrusted party. Even knowing that privileged containers are less secure, if you still must create privileged containers or they are specifically required for your use case, then creating them is quite simple. By default, LXC will create privileged containers.

Note that the terminal prompts we use here may be different than you see on your computer. The terminal prompts we use here emphasize if we are currently in a host shell or container shell and which user we are.

Create a privileged container with the following command. You can choose any container name that will be memorable for you. LXC's download template will help you select a container image available from https://images.linuxcontainers.org/

root@host:~# lxc-create --name mycontainer --template download

If you know the container image you want to use, you can specify the options to be sent to the download template. For example,

root@host:~# lxc-create --name mycontainer --template download -- --dist alpine --release 3.19 --arch amd64

After creating the container, you can start it.

root@host:~# lxc-start --name mycontainer

You can see status information about the container.

root@host:~# lxc-info --name mycontainer
Name:           mycontainer
State:          RUNNING
PID:            3250
IP:             10.0.3.224
Link:           vethgmeH9z
 TX bytes:      1.51 KiB
 RX bytes:      2.15 KiB
 Total bytes:   3.66 KiB

You can see status information about all containers.

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 0         -      10.0.3.224 -    false

Start a container shell.

root@host:~# lxc-attach --name mycontainer

Inside the container is where we really get a feeling for what a system container is and how it is like a lightweight virtual machine in many ways. The changes we make inside the container persist. If we later stop the container and restart it, our changes will still be there.

Explore the container.

root@mycontainer:~# cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.0
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

Update the package index, upgrade installed packages, and install more packages you would like available.

root@mycontainer:~# apk update

root@mycontainer:~# apk add --upgrade apk-tools

root@mycontainer:~# apk upgrade --available

root@mycontainer:~# apk add vim python3

Exit the container shell.

root@mycontainer:~# exit

You can stop the container.

root@host:~# lxc-stop --name mycontainer

If you will never need the container again, then you can permanently destroy it.

root@host:~# lxc-destroy --name mycontainer

Autostart

By default, containers do not start automatically when the host restarts. We may have a service like a web app in the container that should always be up and running. We would like the container to start when the host starts.

Suppose we have already created and started a container named mycontainer as described above.

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 0         -      10.0.3.30  -    false

We can reconfigure the container to autostart by added a line to the container's configuration.

root@host:~# echo "lxc.start.auto = 1" >>/var/lib/lxc/mycontainer/config

After configuring the container, we can reboot the host to test that the container does, in fact, autostart.

root@host:~# reboot

After allowing the host some time to reboot and signing back into the host's shell, we see that the container is running and has the autostart property set to 1.

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 1         -      10.0.3.30  -    false

It works!

If we want several of the containers we create to have autostart, then we might prefer to create a new configuration file to use with lxc-create.

root@host:~# cp /etc/lxc/default.conf /etc/lxc/autostart.conf

root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/autostart.conf

root@host:~# lxc-create --name containera --config /etc/lxc/autostart.conf --template download -- --dist alpine --release 3.19 --arch amd64

As yet another option, if we want all of our containers to autostart, then we can modify the default LXC configuration directly.

For safe keeping, create a backup of the original LXC default.conf file.

root@host:~# cp /etc/lxc/default.conf /etc/lxc/default.conf.original

Now modify the default configuration.

root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/default.conf

All containers we create using the default configuration file from now on will have autostart. For example,

 root@host:~# lxc-create --name containerb --template download -- --dist alpine --release 3.19 --arch amd64

IP Address

Above, the output of lxc-info --name mycontainer and lxc-ls --fancy have shown us that mycontainer has an IP address on the host's local network.

If we start a container and check the output of lxc-ls immediately, we will see that the container does not yet have an IP address.

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer && lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
mycontainer RUNNING 1         -      -    -    false

If we wait about 5 seconds and check again, then the container does have an IP address.

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED
mycontainer RUNNING 1         -      10.0.3.152 -    false

If the container does not have an IP address, we may need to configure the firewall. For example, on Ubuntu 22.04

root@host:~# ufw allow in on lxcbr0
root@host:~# ufw route allow in on lxcbr0
root@host:~# ufw route allow out on lxcbr0

where the value lxcbr0 comes from LXC_BRIDGE in /etc/default/lxc-net.

If we are going to do something in the container that requires access to the Internet, we need to wait until the container has an IP address. One possibilty is to poll the output of lxc-info until it includes an IP address.

root@host:~# lxc-start --name mycontainer

root@host:~# while ! lxc-info -n mycontainer | grep -Eq "^IP:\s*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\s*$"; do sleep 1; done; echo "Container connected!"
Container connected!

Notice that the IP address 10.0.3.152 is not the same as the IP address 10.0.3.30 that we saw earlier. This is because the IP address is dynamically assigned by the host to the container when the container joins the network.

We can see the current list of leases with the following.

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases 
1705896165 8e:e0:fc:72:79:65 10.0.3.152 mycontainer 01:8e:e0:fc:72:79:65

Stopping the container removes the lease.

root@host:~# lxc-stop --name mycontainer;

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases

Restarting the container creates a new lease possibly with a different IP address.

root@host:~# lxc-start --name mycontainer

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases
1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73

Don't try this at home but force destroying a container does not clear the container's lease. Be kind. Always stop a container before destroying it.

root@host:~# lxc-destroy --force --name mycontainer

root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases
1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73

DHCP Reservation

We may need a predictable IP address for the container. We can make a DHCP reservation on the host so the container is assigned the same IP address each time the container joins the local network.

To enable DHCP reservations, we uncomment the LXC_DHCP_CONFILE line in /etc/default/lxc-net.

root@host:~# sed -i 's|^#LXC_DHCP_CONFILE=.*$|LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf|' /etc/default/lxc-net

Add the DHCP reservation.

root@host:~# echo "dhcp-host=mycontainer,10.0.3.100" >>/etc/lxc/dnsmasq.conf

Note: the IP address (i.e. 10.0.3.100 in the command above) must be within LXC_DHCP_RANGE. To see LXC_DHCP_RANGE, open /etc/lxc/dnsmasq.conf. Suppose LXC_DHCP_RANGE="10.0.1.2,10.0.1.254". Then the command above should be

root@host:~# echo "dhcp-host=mycontainer,10.0.1.100" >>/etc/lxc/dnsmasq.conf

instead of the command with 10.0.3.100. Moreover, the IP address must not already be in use. One way to pick an available IP address is use one of the addresses assigned dynamically while working through the section above.

Restart the lxc-net service so the DHCP reservation is enabled.

root@host:~# service lxc-net restart

Restart the container. (You may need to recreate the container if you destroyed it somewhere along the way.)

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer

Wait a few seconds and then check the container's IP address.

root@host:~# lxc-ls --fancy
NAME        STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
mycontainer RUNNING 1         -      10.0.3.100 -    false

Yay! Now we can depend on the container always having the same IP address.

Add a Volume Mount

A container's file system activity is restricted to /var/lib/lxc/<container-name>/rootfs. When a container is destroyed all of /var/lib/lxc/<container-name> is also destroyed. You may have multiple containers and would like to share some file system space between them. You may have disposable containers and would like some file system space to outlive the container. In cases like these, you can create a host volume outside the container's rootfs and then mount that volume inside the container.

Suppose we have already created and started a container named mycontainer as described above.

Create the host volume.

root@host:~# mkdir -p /host/path/to/volume

To mount the volume inside the container there are two options.

The first option requires two steps: manually create the mount target inside the container and then configure the container mount.

root@host:~# lxc-attach --name mycontainer -- mkdir -p /container/mount/point

root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind 0 0" >>/var/lib/lxc/mycontainer/config

The second option requires only one step: use create=dir when configuring the mount so that the mount target is automatically created inside the container for you.

root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind,create=dir 0 0" >>/var/lib/lxc/mycontainer/config

With either option, note that the container mount target path container/mount/point is relative. It does not have a leading / character.

After configuring the container, restart it so the new configuration is used.

root@host:~# lxc-stop --name mycontainer

root@host:~# lxc-start --name mycontainer

Now that we have created the volume and mounted it in the container, we can test that it works.

On the host, add a text file in the volume.

root@host:~# echo "host message" >/host/path/to/volume/messages.txt

Start a container shell.

root@host:~# lxc-attach --name mycontainer

The container can see the text file and its content.

root@mycontainer:~# cat /container/mount/point/messages.txt
host message

The container can add text to the text file.

root@mycontainer:~# echo "mycontainer message" >>/container/mount/point/messages.txt

Exit the container shell.

root@mycontainer:~# exit

The host can see the container's message.

root@host:~# cat /host/path/to/volume/messages.txt 
host message
mycontainer message

Create Unprivileged Containers as Root with Shared UID and GID Ranges

Creating system-wide unprivileged containers (that is, unprivileged containers created and started by root) requires only a few extra steps to organize subordinate user IDs (uid) and subordinate group IDs (gid).

Specifically, you need to manually allocate the subordinate uid and gid ranges to root in /etc/subuid and /etc/subgid and then set those ranges in /etc/lxc/default.conf using lxc.idmap entries.

For example, if you have not done anything on your host related to subordinate uid and gid ranges, the following commands may be all you need. Before doing the following, take a look in /etc/subuid and /etc/subgid to see that the range 100000:65536 is not already in use on your host. If the range is in use, you can use another range.

echo "root:100000:65536" >>/etc/subuid
echo "root:100000:65536" >>/etc/subgid
echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/default.conf
echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/default.conf

That's it! Any container you create as root from now on will be running unprivileged. For example,

lxc-create --name container1 --template download
lxc-create --name container2 --template download

Note that all containers created using the modified default configuration in /etc/lxc/default.conf will share the same subordinate uid and gid ranges. This may not be as secure as each container having its own subordinate uid and gid ranges.

If you start a container, you can explore the uid range in use as seen from the host side compared to the uid range as seen from the container side.

lxc-start --name container1
ps aux
lxc-attach --name container1 -- ps aux

Create Unprivileged Containers as Root with Separate UID and GID Ranges

By using separate subordinate uid and gid ranges for each container, a security breach in one container will not have access to other containers.

Suppose we want to have two containers, we could do the following. (This assumes /etc/lxc/default.conf has not been modified as described above.)

Configure and create the first container with its own uid and gid ranges.

echo "root:100000:65536" >>/etc/subuid
echo "root:100000:65536" >>/etc/subgid
cp /etc/lxc/default.conf /etc/lxc/container1.conf
echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/container1.conf
echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/container1.conf
lxc-create --config /etc/lxc/container1.conf --name container1 --template download

Configure and create the second container with different uid and gid ranges.

echo "root:200000:65536" >>/etc/subuid
echo "root:200000:65536" >>/etc/subgid
cp /etc/lxc/default.conf /etc/lxc/container2.conf
echo "lxc.idmap = u 0 200000 65536" >>/etc/lxc/container2.conf
echo "lxc.idmap = g 0 200000 65536" >>/etc/lxc/container2.conf
lxc-create --config /etc/lxc/container2.conf --name container2 --template download

After creating the containers, you can optionally delete the configuration files /etc/lxc/container1.conf and /etc/lxc/container2.conf.

Create Unprivileged Containers as a User

Unprivileged containers are the safest containers. Those use a map of uid and gid to allocate a range of uids and gids to a container. That means that uid 0 (root) in the container is actually something like uid 100000 outside the container. So should something go very wrong and an attacker manages to escape the container, they'll find themselves with about as many rights as a nobody user.

Unfortunately this also means that the following common operations aren't allowed:

  • mounting of most filesystems
  • creating device nodes
  • any operation against a uid/gid outside of the mapped set

Because of that, most distribution templates simply won't work with those. Instead you should use the "download" template which will provide you with pre-built images of the distributions that are known to work in such an environment.

The following instructions assume the use of a recent Ubuntu system or an alternate Linux distribution offering a similar experience, i.e., a recent kernel and a recent version of shadow, as well as libpam-cgfs and default uid/gid allocation.

First of all, you need to make sure your user has a uid and gid map defined in /etc/subuid and /etc/subgid. On Ubuntu systems, a default allocation of 65536 uids and gids is given to every new user on the system, so you should already have one. If not, you'll have to use usermod to give yourself one.

Next up is /etc/lxc/lxc-usernet which is used to set network devices quota for unprivileged users. By default, your user isn't allowed to create any network device on the host, to change that, add:

echo "$(id -un) veth lxcbr0 10" | sudo tee -a /etc/lxc/lxc-usernet

This means that "your-username" is allowed to create up to 10 veth devices connected to the lxcbr0 bridge.

With that done, the last step is to create an LXC configuration file.

  • Create the ~/.config/lxc directory if it doesn't exist.
  • Copy /etc/lxc/default.conf to ~/.config/lxc/default.conf
  • Append the following two lines to it:
    • lxc.idmap = u 0 100000 65536
    • lxc.idmap = g 0 100000 65536

Those values should match those found in /etc/subuid and /etc/subgid, the values above are those expected for the first user on a standard Ubuntu system.

mkdir -p ~/.config/lxc
cp /etc/lxc/default.conf ~/.config/lxc/default.conf
MS_UID="$(grep "$(id -un)" /etc/subuid  | cut -d : -f 2)"
ME_UID="$(grep "$(id -un)" /etc/subuid  | cut -d : -f 3)"
MS_GID="$(grep "$(id -un)" /etc/subgid  | cut -d : -f 2)"
ME_GID="$(grep "$(id -un)" /etc/subgid  | cut -d : -f 3)"
echo "lxc.idmap = u 0 $MS_UID $ME_UID" >> ~/.config/lxc/default.conf
echo "lxc.idmap = g 0 $MS_GID $ME_GID" >> ~/.config/lxc/default.conf

The current Ubuntu LTS 20.04 requires this extra step:

export DOWNLOAD_KEYSERVER="hkp://keyserver.ubuntu.com"

And now, create your first container with:

systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-create --name mycontainer --template download

The download template will show you a list of distributions, versions, and architectures to choose from. A good example would be "ubuntu", "focal" (20.04 LTS), and "amd64". Another good example would be "alpine", "3.19", and "amd64".

To run unprivileged containers as an unprivileged user, the user must be allocated an empty delegated cgroup (this is required because of the leaf-node and delegation model of cgroup2, not because of liblxc). See cgroups: Full cgroup2 support for more information.

It is not possible to simply start a container from a shell as a user and automatically delegate a cgroup. Therefore, you need to wrap each call to any of the lxc-* commands in a systemd-run command. For example, to start a container, use the following command instead of just lxc-start mycontainer:

systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-start --name mycontainer

NOTE: If libpam-cgfs was not installed on the host machine prior to installing LXC, you need to ensure your user belongs to the right cgroups before creating your first container. You can accomplish this by logging out and logging back in, or by rebooting the host machine.

You can then confirm its status with either of:

lxc-info --name mycontainer
lxc-ls --fancy

And get a shell inside it with:

lxc-attach --name mycontainer

Stopping it can be done with:

lxc-stop --name mycontainer

And finally removing it with:

lxc-destroy --name mycontainer

Distribution LXC Documentation