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!