Linux Containers (LXC) on Slackware
2120 words, 10 minutes
I went to a trip for replacing my $HOME FreeBSD server with loads of jails and Linux containers with some Slackware Linux server with… isolated applications; $REASONS don’t matter much.
After testing Docker and podman , I was not entirely satisfied. After seeking for recommendations on the Fediverse , I was targetted at LXC . This was a brand new technological experience but it felt like a good idea as Proxmox features it too .
Those are my notes about what I learned so far; like I’ve been using it for a couple of weeks or so and have not needed updating this content.
Host preparation
You should probably spend 10 minutes reading rather than 10 days debugging:
My host is running the stable Slackware 15.0/amd64. LXC is part of the
default install, if you install all AP/ packages. Checking the
available kernel features and enabling the RC script provides automatic
containers management, if required.
# lxc-checkconfig
LXC version 4.0.12
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
Ipc namespace: enabled
Pid namespace: enabled
User namespace: enabled
Network namespace: enabled
--- Control groups ---
Cgroups: enabled
Cgroup namespace: enabled
(...)
# chmod +x /etc/rc.d/rc.lxc
# cat <<EOF >>/etc/rc.d/rc.local
# Start LXC:
[ -x /etc/rc.d/rc.lxc ] && /etc/rc.d/rc.lxc start
EOF
# cat <<EOF >>/etc/rc.d/rc.local_shutdown
# Stop LXC:
[ -x /etc/rc.d/rc.lxc ] && /etc/rc.d/rc.lxc stop
EOF
The default LXC storage is /var/lib/lxc.
I don’t want to use the default EXT4 storage for containers. As I have some ZFS pools available, I created a dedicated ZFS dataset for those. All containers and configurations will be stored there.
# zfs create -o mountpoint=/lxc rpool/lxc
# cat <<EOF >>/etc/lxc/lxc.conf
lxc.lxcpath = /lxc
lxc.bdev.zfs.root = rpool/lxc
EOF
# /etc/rc.d/rc.lxc start
Container images
By default, containers will run as root. Services that run inside those containers can still be ran with an unprivileged user.
Public container images are available and hosted at https://images.linuxcontainers.org/ . Those images can be used to deploy various Linux distributions as containers.
The images list can be queried during containers creation:
# lxc-create --bdev zfs --name test --template download
That command lists all the available Linux distributions, versions and architecture. Once you’ve identified one that you like best, they can be selected from the command line to deploy new containers.
# lxc-create -n samba -t download -B zfs -- -d alpine -r 3.24 -a amd64
The deployed containers can be listed from the console using the adequate command:
# lxc-ls -f
NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
samba STOPPED 0 - - - false
test STOPPED 0 - - - false
The commands to start and stop containers are self explained:
# lxc-start -n samba
# lxc-stop -n samba
When a container is running, one can connect to it using remote session
like SSH, via a console-like access using lxc-console or by starting a
command inside the container. To get a shell into the container, simply
use:
# lxc-attach -n samba
Note that lxc-console requires a classical authentication while
lxc-attach give you direct access to a root shell.
Network bridge
By default, a container won’t get any network access.
My first need was to expose services running in containers on my LAN. To achieve this, a bridge interface has to be setup. This bridge will also be used by some of the (KVM) virtual machines.
Configuring the network is briefly explained in the
SlackBook
and inside the rc.inet1.conf configuration file.
# vi /etc/rc.d/rc.inet1.conf
(...)
IPADDRS[0]=""
USE_DHCP[0]=""
IP6ADDRS[0]=""
USE_SLAAC[0]=""
USE_DHCP6[0]=""
DHCP_HOSTNAME[0]=""
(...)
GATEWAY="192.0.2.254"
GATEWAY6=""
(...)
IFNAME[0]="br0"
BRNICS[0]="eth0"
IFOPTS[0]=""
IPADDRS[0]="192.0.2.76/24"
USE_DHCP[0]=""
DHCP_HOSTNAME[0]=""
(...)
A reboot later, the br0 bridge is available for containers and virtual
machines.
Looking at the container’s config, the reason for not having default network access is pretty obvious:
# grep lxc.net /lxc/samba/config
lxc.net.0.type = empty
To allow the container being part of my LAN, it has to be configured to
use the br0 bridge that was created above.
# lxc-stop -n samba
# vi /lxc/samba/config
(...)
lxc.net.0.type = veth
lxc.net.0.link = br0
lxc.net.0.flags = up
(...)
# lxc-start -n samba
A couple of seconds later, the container got an IP address from my LAN’s DHCP server.
# lxc-ls -f
NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
samba RUNNING 1 - 192.0.2.69 - false
test STOPPED 0 - - - false
This container being an Alpine instance, switching to fixed IP is done as described in the documentation . The exact set of operations depends on the container’s distro.
For more “complex” network configuration, like isolating containers in their own subnets and/or vlan etc, it seems all happens on the host; using another bridge, dnsmasq, iptables etc. I didn’t experiment as I had no usage for this (yet).
Persistent storage
By default, the container only sees what’s under its root file system. Also note that when a container gets destroyed, all of its data is also deleted. Persistent data must be stored out of the container’s root. This is achieved by specifying a bind mount.
# cat <<EOF >>/lxc/samba/config
lxc.mount.entry = /host_data data none bind,ro,create=dir 0 0
EOF
This mounts the /host_data directory as /data into the container’s
root file system. It is only accessible read-only from the container. If
the /data directory doesn’t already exist, it is created when the
container starts. Its content will persist after a container’s
destruction.
Autostart
In order to have the container starting automatically when the host boots, the container’s configuration must be amended.
# echo "lxc.start.auto = 1" >> /lxc/samba/config
This will only works if /etc/rc.d/rc.lxc is called. That’s the reason
I added a call in /etc/rc.d/rc.local.
Multi-service container
Running Samba, for example, in an Alpine containers is no different from running it inside a server. It is documented on their Wiki page .
TL;DR goes:
# apk update && apk upgrade
# apk add avahi samba
# rc-update add avahi-daemon
# rc-update add samba
# vi /etc/avahi/services/smb.service
# vi /etc/samba/smb.conf
# rc-service avahi-daemon start
# rc-service samba start
From there, the Samba service is announced on my LAN and can be used by various clients. Any other service is managed the same way. Details obviously depend on your containerized Linux distro.
Also note that, depending on your distribution, some other services may
be running; think cron, syslogd, sshd or systemd.
Samba and Avahi are the kind of services that benefits from persistent storage. From the host’s point-of-view, there are ZFS datasets that host the LXC config, the LXC root file system and some persistent configuration files:
# find /lxc/samba*
/lxc/samba
/lxc/samba/rootfs
/lxc/samba/config
/lxc/samba-conf
/lxc/samba-conf/smb.conf
/lxc/samba-conf/smb.service
# grep mount /lxc/samba/config
lxc.mount.entry = /samba data none bind,ro,create=dir 0 0
lxc.mount.entry = /lxc/samba-conf/smb.conf etc/samba/smb.conf none bind,ro,create=file 0 0
lxc.mount.entry = /lxc/samba-conf/smb.service etc/avahi/services/smb.service none bind,ro,create=file 0 0
Single-process container
When using Zones and Jails, I like to have only one service running per instance. This can also be done with LXC containers.
Say we want to run rsnapshot in our container, using an Alpine base.
The first step is to create the container, provide network and storage
to it.
# lxc-create -n rsnapshot -t download -B zfs -- -d alpine -r 3.24 -a amd64
# sed -i -e 's/^lxc.net.0.type = empty/lxc.net.0.type = veth\
lxc.net.0.link = br0\
lxc.net.0.flags = up/' /lxc/rsnapshot/config
# cat <<EOF >>/lxc/rsnapshot/config
lxc.mount.entry = /var/backup/rsnapshot root/rsnapshot none bind,ro,create=file 0 0
lxc.mount.entry = /var/backup/conf etc/rsnapshot.conf.d none bind,ro,create=dir 0 0
lxc.mount.entry = /var/backup/data var/backup none bind,create=dir 0 0
EOF
Install the required packages in the container. Then modify its
configuration to that it only runs the /root/rsnapshot script when it
starts.
# cat <<EOF >>/var/backup/rsnapshot
#!/bin/sh
/sbin/ip link set lo up
/sbin/ip link set eth0 up
/sbin/udhcpc -i eth0
for i in /etc/rsnapshot.conf.d/*conf; do
/usr/bin/rsnapshot -c $i daily
done
exit 0
EOF
# chmod 0754 /var/backup/rsnapshot
# echo "lxc.init.cmd = /root/rsnapshot" >> /lxc/rsnapshot/config
To check what happens, the container can be run in the foreground.
# lxc-start -n rsnapshot -F
As soon as the init process terminates, the container is stopped. I’m using the host’s crontab to daily run such process at night.
Unprivileged containers
By default, the containers’ root user will have the same UID as the host’s one. Which means, if a malicious process can get out of a container, it may become root on the host.
To force the container root user to be unprivileged on the host, some extra configuration is needed to match the container’s root UID with some host’s UID (different from 0).
# lxc-destroy -n test
# echo "root:100000:65536" >>/etc/subuid
# echo "root:100000:65536" >>/etc/subgid
# cat <<EOF >>/tmp/lxc-unprivileged.conf
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
EOF
# lxc-create -n test -f /tmp/lxc-unprivileged.conf \
-t download -B zfs -- -d alpine -r 3.24 -a amd64
# lxc-execute -n test top
# ps -aef | grep top
root 8032 980 0 12:17 pts/2 00:00:00 lxc-execute -n test top
100000 8037 8033 0 12:17 pts/0 00:00:00 top
The top command is run using the defined UID. Using the lxc-start -n test command starts a whole system in the container. All root
commands (syslogd, crond, etc) are running using the configured UID.
In this configuration, programs running inside the container can still bind to ports <1024. Which means one can expose SSH, Samba or HTTP services, on the LAN, from the containers, using standard ports.
I have a use case where some unprivileged host’s data need to be writeable from some unprivileged user inside an unprivileged container. This can be achieved by mapping the relevant UID. Thanks to Steve for his detailed article .
# cat <<EOF >>/etc/subuid
root:200000:65536
root:1976:1
EOF
# cat <<EOF >>/etc/subgid
root:200000:65536
root:100:1
EOF
# cat <<EOF >>/tmp/lxc-unprivileged.conf
lxc.idmap = u 0 200000 1976
lxc.idmap = u 1976 1976 1
lxc.idmap = u 1977 201976 63560
lxc.idmap = g 0 200000 100
lxc.idmap = g 100 100 1
lxc.idmap = g 101 200101 65435
EOF
# lxc-create -n unpriv -f /tmp/lxc-unprivileged.conf \
-t download -B zfs -- -d alpine -r 3.24 -a amd64
This configuration allows the container and the host to share UID 1976
and GID 100.
The mapping table can be displayed this way:
LXC UID <=> HOST UID | LXC GID <=> HOST GID
=====================|=====================
0 <=> 200000 | 0 <=> 200000
1 <=> 200001 | 1 <=> 200001
2 <=> 200002 | 2 <=> 200002
... | ...
1975 <=> 201975 | 99 <=> 200099
1976 <=> 1976 | 100 <=> 100
1977 <=> 201977 | 101 <=> 200101
... | ...
65535 <=> 265535 | 65535 <=> 265535
On the host, data belong to UID 1976 and GID 100. GID 100 is the default
users group. UID 1976 doesn’t need to be bound to a real host user.
Services running as root inside the container use the host’s UID
200000.
In the container, a user is created using UID 1976 and GID 100. It can access the exported host data. The Samba permissions set regarding to the container’s users allow access to the data from the LAN.
Slackware container
There are available Slackware images for 15.0 and current. As for Alpine, they can be deployed quite simply.
# lxc-create -n slack150 -t download -B zfs -- \
-d slackware -r 15.0 -a amd64
# lxc-create -n slackCurrent -t download -B zfs -- \
-d slackware -r current -a amd64
Once started, with network enabled, those images only have init,
agetty and dhcpcd running. The deployed images are quite small:
74-75 packages, using 344MB-543MB of storage.
Packages can be installed using slackpkg and SBo tools. Feels like
SlackHome when connected to a shell inside the container.
It is also possible to use the local
/usr/share/lxc/templates/lxc-slackware LXC template and download
packages straight from a Slackware mirror.
# arch=x86_64 release=15.0 \
MIRROR=http://ftp.lip6.fr:/pub/linux/distributions/slackware \
lxc-create -n slackest -t slackware -B zfs
This creates a Slackware image with 69 packages installed than weight
208MB. It looks more like a default installation on classical server:
syslogd, dbus-daemon, sshd are already running. There is no
network connectivity until you configure /etc/rc.d/rc.inet1.conf. Once
there, slackpkg fails because it is not able to verify GPG signature
on CHECKSUMS.md5. There are probably a few things to tweak before the
container is fully operational.
Using the lxc-slackware template from current’s lxc-7.0.0-x86_64-2
package, the GPG signature still occurs. So the installation is probably
a bit too stripped down.
Here’s a comparison of the base images size depending on available distributions. Nothing seems to compete with Alpine when it comes to storage usage. Still, using containers for other distros can be useful as overhead is probably way lower than using virtual machines.
# du -sh /lxc/*/rootfs | sort -n
7.4M /lxc/alpine3.24/rootfs
215M /lxc/devuan6/rootfs
225M /lxc/rockylinux10/rootfs
231M /lxc/centos10/rootfs
244M /lxc/debian13/rootfs
326M /lxc/fedora44/rootfs
329M /lxc/ubuntu26.04lts/rootfs
344M /lxc/slackware15.0/rootfs
543M /lxc/slackware-current/rootfs
Now, to your keyboard, Slackers!