Exploring simple Linux containers with lxc
If you ask sysadmins why they love Linux, one of the early answers you’ll get is flexibility. It seems that Linux tools tend to be built with just the right amount of effort having been expended for you, leaving just the right amount of work for you to do on your own. Because a sysadmin is frequently getting asked to solve problems that don’t already have an obvious solution, Linux makes for the ideal building block.
When containers became a household name, not many people knew what to do with them or what was even possible. If anything, they seemed wholly contrary to Linux’s usual philosophies. Many a sysadmin struggled with the idea of editing a YAML file and then rebuilding a container that, itself, refused to store persistent config files of its own.
Now that the dust has settled, and sysadmins are able to see containers for the infinite-scale Linux systems they are, the goal is to bring containers out of specialized industries and into common workflows. In other words, containers aren’t just for CI/CD and sysadmins any more They’re toolkits that normal users and developers can use. And it’s because it leans toward the generic that the lxc project is ideal for everyday container use.
In fact, lxc itself was the foundation that Docker was built upon, and today there are plenty of platforms that leverage the work of lxc both directly and indirectly. Lxc, unlike other container solutions, doesn’t impose a specific daemon or toolchain. Lxc is so serious about fitting into your workflow that it provides Python3 bindings so you can build tooling around it.
If you learn about lxc, you can integrate generic Linux containers into your own system design to solve whatever problem you think a container can solve.
If it’s not already installed, you can install lxc with your package manager:
$ sudo dnf install lxc lxc-templates lxc-doc \
libcgroup-pam libcgroup-tools libcgroup
Containers aren’t actual physical containers, of course, they’re just namespaces. Namespaces are meant to limit what a process “trapped” inside of a container are able to do on a system (specifically, it should only be able to do what its parent container specifies). To make sure your container infrastructure properly cripples processes that aren’t actual system users, verify that your user has a UID and GID map defined in
$ cat /etc/subuid
It’s common for a distribution to allot 65536 UIDs and GIDs to each user. Should a process happen to get outside a container launched by user
seth (in this example), it would be given a UID from 100000 to 165535, so it would find itself with no permissions.
Virtual network interface
A container assumes a network is available, and most of your interactions with a container are over a network connection, even if that network is a local software-defined network interface. In order to create virtual network cards, a user must have permission to do so, and that’s not the default setting for most Linux user accounts.
If it doesn’t already exist, create the
/etc/lxc/lxc-usernet file, used to set network device quotas for unprivileged users. By default, your user account isn’t allowed to create any network devices, but if you want to create and use containers you need to grant yourself the appropriate permissions. Add your user to the
/etc/lxc/lxc-usernet file, along with the network device, bridge, and count:
seth veth virbr0 24
In this example, the user
seth is now permitted to create up to 24
veth devices connected to the
virbr0 network bridge. The
veth device refers to a virtual ethernet card, and the virbr0 device is a virtual bridge. A virtual bridge is, more or less, the software equivalent of a network switch, or a Y-adapter for headphone plugs or power cables.
Containers are defined by configuration files. If you’ve ever built a physical computer and installed Linux onto it, then you can think of a container config as the lxc version of that process. If you’ve used a Kickstart file on RHEL or Fedora, or an Ansible file on any Linux distribution, then you’ll have no trouble understanding a container config. Whether or not you have any of those experiences, you’ll be happy to know that the lxc project provides a starter config file for you to build upon.
First, create a the required local directories:
$ mkdir -p $HOME/.config/lxc
$ mkdir -p $HOME/.cache/lxc
$ cat /etc/lxc/default.conf > $HOME/.config/lxc/default.conf
Now append information about your UID and GID map to the config file. Assuming you are the first user on your host system:
$ echo "lxc.idmap = u 0 100000 65536" >> $HOME/.config/lxc/default.conf
$ echo "lxc.idmap = g 0 100000 65536" >> $HOME/.config/lxc/default.conf
Open it in a text file and make these changes:
lxc.net.0.type = veth
lxc.net.0.link = virbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
If you’re not the first user, or if the values for UIDs and GIDs differ on your system, adjust the values according to what you have in
Reboot your system, and then log back in. Technically, you should be able to only log out, but a reboot is certain. To ensure that your user permissions have been updated.
Creating an lxc container
Once you’ve logged back in, you can create your first container using the
lxc-create command. Setting the template to
download prompts lxc to download a list of available base configurations, including CentOS and Fedora.
$ lxc-create --template download --name penguin
When prompted, enter your desired distribution, release, and architecture. The rootfs and image index is downloaded, and your first container is created.
Starting your container
You only have one now, but eventually, you may gather more, so if you need to list available containers on your system, use
$ lxc-ls --fancy
NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
penguin STOPPED 0 - - - true
To start a container:
lxc-start --daemon --name penguin
You can verify that a container is running with the
$ lxc-ls --fancy
You have started the container, but you have not attached to it. Attach to it by name:
$ sudo lxc-attach --name penguin
It’s not always easy to tell when you’re in a container. A few clues are revealed by
From within the container:
$ ip a show | grep global
inet 192.168.122.8/24 brd 192.168.122.255 [...]
$ uname -av
Linux penguin 5.4.10-200.fc31.x86_64 #1 SMP [...]
From outside the container:
$ ip a show | grep global
inet 10.1.1.5/24 brd 10.1.1.31 scope global [...]
$ uname -av
Linux fedora31 5.4.10-200.fc31.x86_64 #1 SMP [...]
You now have a container ready for development, or for use as a sandbox, or a training environment, or whatever else you want to do with your lxc sandbox.
When you’re finished, exit the container and shut it down:
$ sudo lxc-stop --name penguin
Containers have changed the way development and hosting works. They’ve made Linux the default choice for the cloud. You don’t have to change the way you work to harness their power, though. With lxc, you can create and develop containers the way that they work for you.
[ New to containers? Download the Containers Primer and learn the basics of Linux containers. ]