Skip to content

Instantly share code, notes, and snippets.

@kekneus373
Last active January 4, 2026 22:11
Show Gist options
  • Select an option

  • Save kekneus373/6153edf5a3754fc865112d2c5cf6c32b to your computer and use it in GitHub Desktop.

Select an option

Save kekneus373/6153edf5a3754fc865112d2c5cf6c32b to your computer and use it in GitHub Desktop.
[😅For Sweats Only] Creating Unprivileged LXC Container on Debian 12

Complete Guide: Creating an LXC Container for Docker on Debian 12 (2025-2026)

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.

Prerequisites

  • 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)

Part 1: Host System Setup

Step 1: Update System and Install LXC

First, ensure your system is up to date and install the LXC package:

sudo apt update && sudo apt upgrade
sudo apt install lxc

Step 2: Verify LXC Configuration

Check that your kernel supports all required LXC features:

sudo lxc-checkconfig

All 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.

Step 3: Configure Unprivileged Container Networking

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-usernet

If 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-usernet

This allows you to create up to 10 veth (virtual ethernet) interfaces attached to the lxcbr0 bridge.

Step 4: Configure User ID Mapping

Create the LXC configuration directory for your user:

mkdir -p ~/.config/lxc

Copy the default LXC configuration:

cp /etc/lxc/default.conf ~/.config/lxc/default.conf

Edit 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/subgid

You should see entries like:

<user>:100000:65536

Step 5: Set Up Host Network Bridge

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/interfaces

Be 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 (dhcpmanual)
  • Add lxcbr0 bridge interface. Important: Replace enp0s25 with your main network interface name (find it with ip 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 networking

OR

sudo /etc/init.d/networking restart

Step 6: Prepare container for static IP

vim ~/.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.

Step 7: Turn off LXC-net services (otherwise you'd be screwed)

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.

  1. 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
  2. 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
  3. 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

Part 2: Creating the LXC Container

Step 8: Create the Container with systemd Delegation

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 download

When 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!

Step 9: Fix Directory Permissions

⚠️ DON'T RUN CT YET

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/.


Part 3: Starting and Accessing the Container

Step 10: Start the Container

Start the container with systemd delegation:

systemd-run --unit=dockerct --user --scope -p "Delegate=yes" -- lxc-start --name dockerct --logfile ~/dockerct.log

Note: ~/dockerct.log is your preferred log file path. Helps troubleshoot startup issues.

Verify the container is running:

lxc-ls --fancy

Step 11: Attach to the Container

Access the container's shell:

systemd-run --user --scope -p "Delegate=yes" -- lxc-attach dockerct

You should now be inside the container as root. There are NO passwords by default!

Step 12: Disable Ubuntu's DHCP

To avoid getting secondary unnecessary IP address for CT, modify the NetPlan config to get rid of DHCP.

vim /etc/netplan/10-lxc.yaml
network:
  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 apply

WARNING
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 🦍


Part 4: Installing Docker Inside the Container

All following steps are done inside the container.

Step 13: Update Container and Install Prerequisites

I hope you have internet connection ping 1.1.1.1.

apt update
apt install ca-certificates curl

Step 14: Add Docker's Official GPG Key

Create the keyrings directory:

install -m 0755 -d /etc/apt/keyrings

Download Docker's GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc

Set proper permissions:

chmod a+r /etc/apt/keyrings/docker.asc

Step 15: Add Docker Repository

Create 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
EOF

Step 16: Install Docker

Update package lists and install Docker:

apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Step 17: Verify Docker Installation

Check Docker service status:

systemctl status docker

Run the hello-world test:

docker run hello-world

If successful, you'll see a message confirming Docker is working correctly.


Part 5: Container Management

Starting the Container Automatically

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.

  1. Before doing that, stop the container (very important!):
systemd-run --user --scope -p "Delegate=yes" -- lxc-stop --name dockerct
  1. 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
  1. After creating the file, enable and start the service:
systemctl --user daemon-reload
systemctl --user enable dockerct.service
systemctl --user start dockerct.service
  1. To ensure the systemd user instance starts at boot rather than only when the user logs in, run:
sudo loginctl enable-linger $USER
  1. If possible, try rebooting:
sudo reboot

After 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  -    true

It works!

Source

Useful Container Management Commands

Stop the container:

systemd-run --user --scope -p "Delegate=yes" -- lxc-stop --name dockerct

Check container status:

lxc-info --name dockerct

List all containers:

lxc-ls --fancy

Destroy the container (WARNING: deletes all data):

lxc-destroy --name dockerct

Troubleshooting

Docker Fails to Start

Issue: 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.

Network Connectivity Issues

Issue: Container has no network access.

Solution:

  1. Verify the bridge is up and what IP it's got: ip addr show lxcbr0
  2. Check container network config:
    • Inside container, run ip addr
    • Look through /etc/netplan/10-lxc.yaml (Ubuntu's network manager config file)
  3. Verify /etc/lxc/lxc-usernet has correct user permissions (not my case)
  4. Re-check every configuration file related to network interfaces on your host + CT
  5. Reload your network stack

Additional Debugging

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 FORWARD

Test connectivity:

# From host to container
ping 192.168.0.55

# From container to gateway
sudo lxc-attach -n container -- ping 192.168.0.3

Restart 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 dockerct

Permission Denied Errors

Issue: Cannot create or start containers.

Solution:

  1. Verify /etc/subuid and /etc/subgid contain entries for your user
  2. Check ~/.config/lxc/default.conf has correct ID mappings and compare it with CT's config ~/.local/share/lxc/dockerct/config
  3. Ensure ~/.local/ and ~/.local/share/ are executable

AppArmor Conflicts

Issue: Container fails to start with AppArmor errors.

Solution: The configuration uses lxc.apparmor.profile = unconfined. If issues persist, check:

sudo aa-status

Consider upgrading to kernel 6.2+ if using Debian Bookworm's default 6.1 kernel (shall not happen in general).

Leftover "Fragments" After Killing or CT Timeout. Fixing w/o reboot

# 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 -k

If cgroups are wedged:

sudo umount -l /sys/fs/cgroup/*

(last resort, but avoids reboot)


Security Considerations

  1. 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.

  2. AppArmor Profile: We use unconfined for simplicity. I agree with AI but that seems to be an upstream bug that prevents LXC CT from successfully booting up

  3. Network Isolation: The bridge configuration exposes the container to your local network. Consider using firewall on both host and container OS. I prefer UFW.

  4. Docker Socket: Be cautious about who has access to the Docker socket inside the container, as it provides significant privileges.


References


Ending note (glad I didn't rage quit)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment