Running unprivileged LXC containers on Fedora
LXC containers are great - they are an easy way to set up a clean development or testing environment, lightweight, and simple to maintain. Unfortunately, running unprivileged containers (i.e. not running them as root, which is inconvenient and generally not a good idea) doesn’t work out-of-the box on Fedora (but to be fair, the same goes for Debian and friends).
At the end of this guide you should be able to run unprivileged containers without needing to mess with SELinux (which strangely the Wiki suggests you need to disable) with the option to add bind mounts in your container to share files between your home folder and the container’s filesystem.
Some basics first
Before we continue we just need to clear up some basics. By running containers
in unprivileged mode, it means that you will never be making use of root
privileges on your host machine. They will be started as your normal user
account - for this to work LXC will make use of something called UID
namespaces. Basically, inside the container, UID 0 (which is root) will be
mapped to some arbitrary UID on the host (usually way up in the thousands).
This has the cool benefit that if something were to break out of the container,
it will be running as some arbitrary, unprivileged UID on the host and probably
won’t be able to do much. The other cool thing is that this allows your
unprivileged normal user account to start, stop and manage containers without
I’m writing this guide on Fedora 34, but it should also work on other versions. Firstly, install LXC and its templates (templates makes creating containers much easier):
sudo dnf install lxc lxc-templates
Setting up the networking
lxcbr0- so be sure to change
lxcbr0in all of the configuration options below.
Next you’ll need to allow your user access to the network bridge created by the LXC networking service. To do so, run the following:
# allow user to access network bridge # see the note regarding F35 above sudo tee /etc/lxc/lxc-usernet <<EOF $USER veth virbr0 10 EOF # restart the lxc network service sudo systemctl restart lxc-net
With these two commands you’ve allowed your user account to create 10 network
bridges on the default LXC network device
virbr0. Without this your
containers won’t be able to attach to the network bridge and they won’t have
Updating the firewall rules on F35
This step is only required if you’re running Fedora 35 or newer. By default,
firewalld will block your containers from obtaining DHCP addresses. To fix
this you’ll need to add the LXC bridge to
firewalld’s trusted zone.
sudo firewall-cmd --zone=trusted --change-interface=lxcbr0 --permanent sudo firewall-cmd --reload
Finally we also need to tell LXC to use the correct network interface, and to
set up the correct UID and GID mappings. You can add the following to the
system-wide LXC config (
/etc/lxc/default.conf) however I prefer using the one
in my home folder so that I can check it into my
Add the following to
~/.config/lxc/default.conf (you might need to create the
# default networking lxc.net.0.type = veth # remember to update this to lxcbr0 on F35 and above lxc.net.0.link = virbr0 lxc.net.0.flags = up lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx # uid and gid mapping - all the ids, except 1000, are mapped to an # unpriviledged range on the host. 1000 is kept so that filesharing through # bind mounts is possible lxc.idmap = u 0 100000 1000 lxc.idmap = u 1000 1000 1 lxc.idmap = u 1001 101000 64536 lxc.idmap = g 0 100000 1000 lxc.idmap = g 1000 1000 1 lxc.idmap = g 1001 101000 64536
One thing to note is the UID mapping ranges. All the container’s UIDs, except
for 1000, is mapped to UIDs >100000. However UID 1000 is mapped to 1000 on the
host - this is to allow read-write bind mounts between your home folder and the
container’s default user (not root! A normal user account that you created -
see the next section). Your account should have an UID of 1000 by default. If
not, you’ll need to slightly adjust the above to fit your account’s UID (
Creating the container
At this point you should be able to create and run a container. To do so run the following:
# here because the keyserver used by LXC no longer exists export DOWNLOAD_KEYSERVER=keys.openpgp.org # create the container - doesn't need to be fedora lxc-create -t download -n my_container -- -d fedora -r 34 -a amd64
If everything went well you should be presented with something like the following:
Setting up the GPG keyring Downloading the image index Downloading the rootfs Downloading the metadata The image cache is now ready Unpacking the rootfs --- You just created a Fedora 34 x86_64 (20210805_20:33) container.
Start the container, and create a normal user account to use:
# start the container lxc-start my_container # attach to it lxc-attach my_container ## we're now inside the container as the root user # create a normal user and set a password useradd -mG wheel -s /usr/bin/bash my_user passwd my_user # log in as the normal user su my_user # create a folder in their home folder mkdir ~/test # log out of the normal user exit # exit the container exit ## we're now back on the host # stop the container lxc-stop my_container
We created the test folder so that we can set up a bind-mount, as explained in the next section.
Bind-mounting folders from the host to the container
Using bind-mounts comes in very handy, for example mounting your code directory
(which is on the host) to inside the container. Since the UID mappings have
already been set up you just need to add the following to your container config
lxc.mount.entry = /home/kobus/test home/my_user/test none bind 0 0
A few things to note here:
- The first part of the line is the absolute path to the folder on the host
system (my username is
kobus, be sure to adjust it accordingly).
- The second part is the path relative to the container’s rootfs - i.e. just
make sure to omit the leading slash
- You can add mounting options instead of the
none. For instance, this can be used to make the mount read-only.
Close and save the file, and that’s it! If you start your container again the bind mount should be up, allowing you to easily share files between your host and container.