This episode covers performance and scale: choosing the ZFS/btrfs backend for efficient snapshots, tuning cgroups for high density, per-container resource planning, and handling hundreds of containers per host along with boot and overhead benchmarks.

For the last five episodes you've been building one host with a few containers. Now it's time to think bigger: how do you make one host serve tens to hundreds of containers while keeping performance healthy? In episode 18 we tune storage, cgroups, and resource planning — then measure the overhead. This is the episode that separates a casual homelab from measurable infrastructure.
Back to episode 8: the backend determines snapshot and clone efficiency. At scale, this is no longer a convenience but a matter of survival. Imagine 100 containers and every full snapshot = hundreds of GB of disk. With a copy-on-write backend, snapshots are nearly free.
ZFS is a popular choice for large LXC hosts:
lxc-snapshot and lxc-copy -s use ZFS's instant mechanism.compression=on saves significant disk space for rootfs that is mostly text files.zfs create -o compression=on -o atime=off tank/containers
zfs set mountpoint=/var/lib/lxc tank/containersbtrfs has similar strengths (native snapshots, copy-on-write) with simpler management. For hosts without deep ZFS feature needs, btrfs is a solid lightweight choice.
Tip
Selection rule: need massive snapshots + clones → ZFS or btrfs. Storage already standardized on LVM → leverage LVM snapshots. All static containers, rarely snapshotted → dir is enough. Consistency of the backend from the start matters more than "the best backend" in the abstract.
Episode 7 stressed limits. At scale, you have to calculate total limits ≤ host capacity:
Host: 16 cores, 64 GB RAM
100 lightweight web containers: 0.1 core + 256 MB each
→ CPU: 10 cores (< 16) OK
→ Memory: 25.6 GB (< 64) OK, the rest for host & buffersThe simple formula: sum all container limits, and make sure to leave room for the host itself (kernel, services, I/O cache).
For bursty workloads, use lxc.cgroup2.cpu.weight (relative) instead of rigid quotas:
lxc.cgroup2.cpu.weight = 100cpu.weight (1-10000) sets proportions during contention without wasting CPU when idle — ideal for high density with wave-like load. Combine with cpu.max only for containers that truly need guarantees.
The Linux host uses memory for I/O page cache. If container limits consume nearly all RAM, the cache shrinks and disk performance tanks. Leave ~10-20% of host RAM for the kernel and cache.
The measurement standard before setting limits:
dd, CPU loop) — prove the limit applies.Example profiles for specific container types:
Light nginx/apache : 256 MB , 0.2 core
Small database : 1 GB , 1 core
CI build : 4 GB , 4 core (bursty)Measure for yourself to base decisions:
time lxc-start -n c1ps -o rss= -p $(pgrep -f "lxc-init" | head -1) 2>/dev/null || trueWarning
At the scale of hundreds of containers, a single global configuration mistake (e.g. a wrong default.conf, or one slow backend) gets multiplied by a hundred. Test a configuration pattern on 2-3 containers first, verify, then propagate. Don't be a beta tester for a configuration that's being scaled out.
ulimit, fs.file-max).lxc-autostart with lxc.start.delay (episode 10) prevents a thunder herd on host reboot.lxc.cgroup2.io.weight (episode 7).Key takeaways:
cpu.weight for bursty loads; cpu.max for guarantees.In the next episode 19 we'll cover the ecosystem: LXD, Incus & Proxmox — LXD as Canonical's LXC manager with a REST API, clustering, and VMs; Incus as the community fork (2023); and the comparison of low-level LXC vs high-level LXD/Incus vs Docker application containers.