AI only formatted this doc and added explanations where it knew. I confirm all the steps here were actually taken by me to achieve the desired result🙂
This guide walks you through setting up an unprivileged LXC container on Debian 12 to run Docker, using systemd for proper cgroup delegation.
- Debian 12 (Bookworm) host system
- Non-root user account with sudo privileges
- Basic understanding of Linux containers and networking
- Physical access to the machine (involves multiple network interfaces refreshes)
- You're fine with static IPs (haven't found other way)
First, ensure your system is up to date and install the LXC package:
sudo apt update && sudo apt upgrade
sudo apt install lxcCheck that your kernel supports all required LXC features:
sudo lxc-checkconfigAll checks should return "enabled". If any are missing, you may need to enable kernel modules or upgrade your kernel. Stable Debian 12 Bookworm is totally compatible.
Allow your user to create network interfaces. If you have sudo (unlike me), then run this command:
echo "$(id -un) veth lxcbr0 10" | sudo tee -a /etc/lxc/lxc-usernetIf you use su, run the commands below one by one. Replace <user> with your actual username:
id -un # outputs your username
su
echo "<user> veth lxcbr0 10" | tee -a /etc/lxc/lxc-usernetThis allows you to create up to 10 veth (virtual ethernet) interfaces attached to the lxcbr0 bridge.
Create the LXC configuration directory for your user:
mkdir -p ~/.config/lxcCopy the default LXC configuration:
cp /etc/lxc/default.conf ~/.config/lxc/default.confEdit the user configuration file:
vim ~/.config/lxc/default.conf-
Modify the AppArmor profile line to avoid issues at startup:
lxc.apparmor.profile = unconfined -
Append the following ID mapping lines:
lxc.idmap = u 0 100000 65536 lxc.idmap = g 0 100000 65536
These values map the container's UIDs/GIDs (0-65535) to host UIDs/GIDs (100000-165535). Verify these match your /etc/subuid and /etc/subgid files:
cat /etc/subuid
cat /etc/subgidYou should see entries like:
<user>:100000:65536
Here comes the tricky part. I personally needed to open my CT to my global network and give it a real IP. This is called host-shared bridge. And I almost lost my mind configuring it 🧠🧽... You were warned 💀
Edit the network interfaces configuration:
sudo vim /etc/network/interfacesBe careful to not mess up your current config (I did it 3 times😭). In any case, you will LOSE SSH ACCESS!
- Set your host to static (
dhcp→manual) - Add
lxcbr0bridge interface. Important: Replaceenp0s25with your main network interface name (find it withip link show). - Adjust IP addresses and DNS settings to match your network configuration.
- Think what you're typing!!!
# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
allow-hotplug enp0s25
iface enp0s25 inet manual
# The LXC bridge
auto lxcbr0
iface lxcbr0 inet static
bridge_ports enp0s25
address 192.168.0.54
netmask 255.255.255.0
network 192.168.0.0
broadcast 192.168.0.255
gateway 192.168.0.3
dns-nameservers 192.168.0.7 192.168.0.3
dns-search MyDom.local
# Note that it is important to include 'bridge_*' to bring the interface up quickly.
# Other values will cause network packets to be dropped for the
# first 30 seconds after the bridge has become active.
# This in turn could prevent CTs from getting internet access.
# Source: https://wiki.gentoo.org/wiki/LXC/Network_examples
bridge_stp off
bridge_fd 0
bridge_maxwait 0
bridge_ageing 0
Restart networking or reboot to apply changes:
sudo systemctl restart networkingOR
sudo /etc/init.d/networking restartvim ~/.local/share/lxc/dockerct/config# Network configuration
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
# the interface name inside CT
lxc.net.0.name = eth0
# insert ur own MAC here (www.browserling.com/tools/random-mac)
lxc.net.0.hwaddr = f4:8a:df:de:ed:be
lxc.net.0.ipv4.address = 192.168.0.55/24
lxc.net.0.ipv4.gateway = 192.168.0.3
# adjust IP addresses and DNS to match your network config.You have USE_LXC_BRIDGE="true" in /etc/default/lxc-net, which starts LXC's built-in DHCP/DNS services (dnsmasq) that conflict with your static bridge configuration. Since we're using lxcbr0 as a real bridge (not LXC's managed bridge), we need to disable LXC's network management. This won't break the user's ability to manage the interface.
-
Disable LXC's Network Management Edit
/etc/default/lxc-net:USE_LXC_BRIDGE="false"Stop and disable the lxc-net service:
sudo systemctl stop lxc-net sudo systemctl disable lxc-net
-
Enable IP Forwarding on Host Check if IP forwarding is enabled:
sysctl net.ipv4.ip_forward
If it returns
0, enable it:echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf sudo sysctl -p
-
Add iptables / ufw Rules for NAT/Forwarding (don't be scared of it) Since we're bridging directly to the network, we 100% need these rules on the host:
# Allow forwarding ### IF YOU'RE USING IPTABLES ### sudo iptables -A FORWARD -i lxcbr0 -o lxcbr0 -j ACCEPT sudo iptables -A FORWARD -i lxcbr0 -j ACCEPT sudo iptables -A FORWARD -o lxcbr0 -j ACCEPT # Save rules (Debian) sudo apt install iptables-persistent sudo netfilter-persistent save ### IF YOU'RE USING UFW (which manages iptables) ### # First, ensure forwarding is enabled globally sudo sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw sudo ufw reload # Allow all traffic to/from the lxcbr0 interface (including between containers) sudo ufw allow in on lxcbr0 sudo ufw allow out on lxcbr0 # Allow routed/forwarded traffic on lxcbr0 (covers forwarding in both directions, including container-to-container) sudo ufw route allow in on lxcbr0 sudo ufw route allow out on lxcbr0 # Reload UFW to apply changes sudo ufw reload
When running ANY lxc command, use systemd-run to ensure proper cgroup delegation for the containerized OS:
systemd-run --unit=dockerct --user --scope -p "Delegate=yes" -- lxc-create --name dockerct --template downloadWhen prompted, select:
- Distribution:
ubuntu - Release:
noble(Ubuntu 24.04 LTS) - Architecture:
amd64
Why systemd-run? LXC requires full cgroup delegation to manage containers. Running LXC commands through systemd-run with Delegate=yes ensures the container gets its own cgroup hierarchy. Or else it will never work!
Ensure the container storage directories are executable:
chmod +x ~/.local/
chmod +x ~/.local/share/This allows the system to access the container's rootfs stored in ~/.local/share/lxc/.
Start the container with systemd delegation:
systemd-run --unit=dockerct --user --scope -p "Delegate=yes" -- lxc-start --name dockerct --logfile ~/dockerct.logNote: ~/dockerct.log is your preferred log file path. Helps troubleshoot startup issues.
Verify the container is running:
lxc-ls --fancyAccess the container's shell:
systemd-run --user --scope -p "Delegate=yes" -- lxc-attach dockerctYou should now be inside the container as root. There are NO passwords by default!
To avoid getting secondary unnecessary IP address for CT, modify the NetPlan config to get rid of DHCP.
vim /etc/netplan/10-lxc.yamlnetwork:
version: 2
ethernets:
eth0:
dhcp4: false
# dhcp-identifier: mac
addresses:
- 192.168.0.55/24
routes:
- to: default
via: 192.168.0.3
nameservers:
addresses: [192.168.0.7, 192.168.0.3]Adjust IP addresses and DNS to match your network configuration. routes via 192.168.0.3 is your gateway (router in most cases), nameservers - DNS. More info on that: https://linuxconfig.org/how-to-configure-static-ip-address-on-ubuntu-18-04-bionic-beaver-linux
After commiting changes, save them:
netplan applyWARNING
You must adhere to a correct code indent for each line of the block. In other words, the number of spaces before each configuration stanza matters. Otherwise you may end up with an error message similar to:
Invalid YAML at //etc/netplan/01-netcfg.yaml line 7 column 6: did not find expected key
If you get an error like that:
/etc/netplan/10-lxc.yaml:6:18: Invalid YAML: inconsistent indentation:
addresses:
^
check if you have correct spacing (no tabs; only DOUBLE spaces as indents just like in my example). here I accidentaly added another doublespace on 6th line and it completely confused me 🦍
All following steps are done inside the container.
I hope you have internet connection ping 1.1.1.1.
apt update
apt install ca-certificates curlCreate the keyrings directory:
install -m 0755 -d /etc/apt/keyringsDownload Docker's GPG key:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.ascSet proper permissions:
chmod a+r /etc/apt/keyrings/docker.ascCreate the Docker sources file (run the whole command AT ONCE.):
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOFUpdate package lists and install Docker:
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginCheck Docker service status:
systemctl status dockerRun the hello-world test:
docker run hello-worldIf successful, you'll see a message confirming Docker is working correctly.
To autostart unprivileged LXC containers, several methods are available depending on the system configuration and init system used. One effective method involves using systemd user services.
- Before doing that, stop the container (very important!):
systemd-run --user --scope -p "Delegate=yes" -- lxc-stop --name dockerct- Create a service file in the user's home directory with the following content:
vim ~/.config/systemd/user/dockerct.service[Unit]
Description="Lxc-autostart for Docker LXC"
[Service]
Type=forking
ExecStart=/usr/bin/lxc-start -n dockerct -d
ExecStop=/usr/bin/lxc-stop -n dockerct
KillMode=none
Delegate=yes
RemainAfterExit=yes
TimeoutStopSec=60
[Install]
WantedBy=default.target- After creating the file, enable and start the service:
systemctl --user daemon-reload
systemctl --user enable dockerct.service
systemctl --user start dockerct.service- To ensure the systemd user instance starts at boot rather than only when the user logs in, run:
sudo loginctl enable-linger $USER- If possible, try rebooting:
sudo rebootAfter 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.
lxcuser@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 0 - 10.0.3.30 - trueIt works!
Stop the container:
systemd-run --user --scope -p "Delegate=yes" -- lxc-stop --name dockerctCheck container status:
lxc-info --name dockerctList all containers:
lxc-ls --fancyDestroy the container (WARNING: deletes all data):
lxc-destroy --name dockerctIssue: Docker service won't start inside the container.
Solution: Ensure you're using systemd-run with Delegate=yes when starting the container and attaching to it. Docker requires full cgroup v2 delegation.
Issue: Container has no network access.
Solution:
- Verify the bridge is up and what IP it's got:
ip addr show lxcbr0 - Check container network config:
- Inside container, run
ip addr - Look through
/etc/netplan/10-lxc.yaml(Ubuntu's network manager config file)
- Inside container, run
- Verify
/etc/lxc/lxc-usernethas correct user permissions (not my case) - Re-check every configuration file related to network interfaces on your host + CT
- Reload your network stack
If ping still doesn't work after this, check:
On the host:
# Verify bridge has the physical interface
brctl show lxcbr0
# Check if packets are being filtered
sudo iptables -L -v -n | grep -A5 FORWARDTest connectivity:
# From host to container
ping 192.168.0.55
# From container to gateway
sudo lxc-attach -n container -- ping 192.168.0.3Restart Everything twin 🗿
# On host
sudo systemctl restart networking
systemd-run --user --scope -p "Delegate=yes" -- lxc-stop -n dockerct
systemd-run --user --unit=dockerct --scope -p "Delegate=yes" -- lxc-start -n dockerctIssue: Cannot create or start containers.
Solution:
- Verify
/etc/subuidand/etc/subgidcontain entries for your user - Check
~/.config/lxc/default.confhas correct ID mappings and compare it with CT's config~/.local/share/lxc/dockerct/config - Ensure
~/.local/and~/.local/share/are executable
Issue: Container fails to start with AppArmor errors.
Solution: The configuration uses lxc.apparmor.profile = unconfined. If issues persist, check:
sudo aa-statusConsider upgrading to kernel 6.2+ if using Debian Bookworm's default 6.1 kernel (shall not happen in general).
# kill leftover cgroups
systemctl --user reset-failed
systemctl --user daemon-reexec
# check LXC state
lxc-info -n lxc1
# if stuck
sudo lxc-stop -n dockerct -kIf cgroups are wedged:
sudo umount -l /sys/fs/cgroup/*
(last resort, but avoids reboot)
-
Unprivileged Containers: This setup uses unprivileged containers, which provide better isolation than privileged containers by mapping container root to a non-root host user. If a hacker breaches your CT, they won't be able to do much innit.
-
AppArmor Profile: We use
unconfinedfor simplicity. I agree with AI but that seems to be an upstream bug that prevents LXC CT from successfully booting up -
Network Isolation: The bridge configuration exposes the container to your local network. Consider using firewall on both host and container OS. I prefer UFW.
-
Docker Socket: Be cautious about who has access to the Docker socket inside the container, as it provides significant privileges.
- Debian LXC Wiki
- LXC/SimpleBridge - Debian Wiki. The most helpful article yet
- LXC Getting Started Guide. Pretty good too.
- Docker Official Installation Guide. Was flawless
- Michael Kelly's LXC on Debian Setup Guide. Great explanation
- Lastly, Chat is GOATED
I'm honestly so pissed of by the amount of disinformation around. Trust me, I read like 30 different guides and forum posts, all of which were either super outdated or missing key stuff like getting rid of DHCP services. "RTFM twin" also was of no help 'cause again what I needed was NOT mentioned. And that's with my decent level of Linux skills and good understanding of what I'm typing. So yeah, setting this up got me sweating hard, be ready... ☣️
Last Updated: 4 January 2026
Tested On: Debian 12 (Bookworm) with Linux kernel 6.1.0-40-amd64
Written with StackEdit.
Written with StackEdit.