A dedicated server with one operating system running every service is a server where each tenant trusts every other tenant, and the moment the tenants are different applications, different clients or different teams, that trust stops being a design and starts being a liability. LXC occupies the middle ground that solves this on plain hardware: it splits a single kernel into multiple complete, isolated Linux systems, each with its own init, users, network stack and filesystem, at a cost measured in megabytes rather than the gigabytes that virtual machines demand. This article builds the real-world practice: creating and running machine containers, the networking models that make them production citizens, the management commands, resource limits, storage and backups, and the decision framework for when LXC is the right tool on a dedicated box.

Machine containers and where they sit in the spectrum

The container spectrum runs from application containers, one process in a cage, to full virtual machines, one kernel in a hypervisor cage, and LXC sits firmly at the machine-container end: each LXC instance is a complete Linux system with its own init process, its own service tree, its own root filesystem and its own user accounts, sharing only the host kernel. The contrast with the app-container model is the point: an LXC container boots, logs in, upgrades packages and runs systemd inside, exactly like the virtual machine it replaces, while the app-container image runs one process and exits. The contrast upward is equally sharp: no guest kernel means no kernel boot cost, no memory reservation for a second kernel, and near-zero performance overhead, at the price of sharing the host's kernel with every other container on the box.

The sharing is the honest trade. All containers on a dedicated server run the same kernel, so a kernel vulnerability is a fleet-wide event and a container escape, historically rare and steadily hardened against, would be a host-level incident. The model fits the dedicated-server use case precisely because the threat model is about separation of concerns, isolating tenants and workloads from each other, rather than about defending a hostile multitenant platform, which is what virtualization layers exist for.

Creating the first container and the management vocabulary

Installation is a package on every distribution, lxc with its toolset, and the first container is one command, choosing a template by name:

lxc-create -n web01 -t debian

which downloads a minimal Debian root filesystem, unpacks it under /var/lib/lxc/web01/rootfs, writes the container's configuration file alongside, and reports the root password or the key setup for the first login. The download template reaches the upstream image server with a broader menu:

lxc-create -n cache01 -t download -- -d debian -r bookworm -a amd64

and the lifecycle commands read like a small vocabulary:

lxc-start -n web01 # lxc-ls --fancy # lxc-attach -n web01 -- apt update # lxc-console -n web01 # lxc-stop -n web01

lxc-attach is the working command of daily life, running any command inside the container without a login, while lxc-console attaches to the container's text console for the login-and-work experience, and lxc-ls --fancy is the status screen, one line per container with its running state, its network address and its autostart flag.

The container's identity lives in two places. The configuration file, /var/lib/lxc/web01/config, holds every declaration: the network setup, the root filesystem path, the cgroup limits, the autostart behavior. The rootfs directory is the container's entire disk, and this duality, one config file and one directory tree, is the whole of LXC's mental model, a simplicity that pays off in the backup and cloning sections below.

Networking for production containers

The default template setup attaches each container to a private bridge with NAT, the container gets a DHCP address from the host's lxcbr0, and outbound connectivity works out of the box while inbound requires published ports. The NAT model suits development, and production on a dedicated server usually graduates to the bridged model, where containers join the physical network as first-class addresses. The configuration reads:

lxc.net.0.type = veth
lxc.net.0.link = br0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:01
lxc.net.0.ipv4.address = 203.0.113.21/24
lxc.net.0.ipv4.gateway = 203.0.113.1

with br0 the bridge the host built on its physical interface, and the static address making the container indistinguishable from a physical server to everything else on the network, DNS records, firewalls and monitoring included. The hwaddr line deserves its habit: assigning stable, recorded MAC addresses prevents the surprise of a container getting a different DHCP lease after a recreation.

The host-side firewall completes the network story, and the iptables OUTPUT article in this series applies unchanged: the container's traffic leaves through the host's stack, so the host's outbound whitelist governs the whole box, while per-container inbound rules are ordinary security-group entries on the bridge or the host interface. A dedicated server running five containers thus has one firewall policy with five destination addresses, rather than five separate per-tenant policies, which is the consolidation that makes the LXC pattern administratively attractive.

Unprivileged containers and the security envelope

The historical criticism of LXC, that containers ran as root sharing the host's root, was answered by the unprivileged container model years ago, and it is the default posture for any production deployment. An unprivileged container maps its internal root to an ordinary host user through the user-namespace machinery, the same subordinate-ID ranges described in the Podman article of this series, so the container's uid 0 holds no host privileges, and the escape scenario that once defined the risk becomes a user-level event.

Creating unprivileged containers is a per-user setup: the user holds subuid and subgid ranges, the LXC configuration under ~/.config/lxc/ declares the idmap and the veth network, and lxc-create under that user builds containers owned and operated entirely without host root. The privileged form remains available for the cases that genuinely need it, containers that must load kernel modules or manage devices, and the decision is per-container rather than per-host, with the default answer being unprivileged and each exception documented in the runbook with its reason.

The nesting capability completes the envelope picture, because a privileged-enough LXC container can run LXC inside itself, and more relevantly for this series, unprivileged LXC containers can run Podman rootless containers inside, the pattern of a dedicated server hosting tenant environments that each run their own app containers, two isolation layers composed deliberately rather than competing.

Resource limits, storage and backups

Resource control on a current system is cgroups v2 through LXC's config syntax, and the knobs are the same files the previous article described, expressed in the container's declaration:

lxc.cgroup2.memory.max = 2G
lxc.cgroup2.cpu.max = 200000 100000
lxc.cgroup2.pids.max = 300

The limits compose with everything the cgroups article established: memory.max as the wall, the cpu quota as the fence, pids.max as the fork-bomb guard, and the observation files under the container's cgroup feeding the same monitoring. A dedicated server with eight containers applies a budget per tenant from the same four-line vocabulary, and the host keeps its reserve because every container declares its ceiling up front.

Storage options span the same range as virtual machines, with the default directory backend being the simplest and the production patterns adding flexibility. LVM volumes give each container a block device with its own filesystem, snapshots through the volume manager, and clean size boundaries. ZFS and Btrfs datasets bring snapshot and send semantics natively, one snapshot of the dataset being a complete, atomic image of the container at that moment. The directory backend remains valid for small fleets, and the backup pattern is the same at every tier: stop the container or quiesce its writes, snapshot the storage, send the snapshot off-box, and test the restore quarterly, the discipline the backup articles of this series cover in depth and that applies to machine containers without modification.

Cloning is the bonus that directory simplicity buys, because a container is one config file and one rootfs tree:

lxc-copy -n web01 -N web02

produces a running-ready sibling with a new name and a new MAC, the pattern for staging environments, blue-green service swaps and the one-off reproduction of a production problem on a private copy, operations that cost minutes each and that the VM world measures in hours.

The dedicated-server pattern and the honest boundaries

The dedicated-server use case where LXC shines has a shape worth naming precisely. One physical box, one host operating system maintained as a hardened, minimal layer, and a set of machine containers above it, each a tenant with its own address, its own package lifecycle and its own firewall expectations. The host runs almost nothing except LXC, the SSH bastion stack from earlier in this series and the monitoring agent, and every service lives inside a container where a compromise, a misconfiguration or a bad upgrade is one tenant's problem rather than the server's.

The boundaries of the model are the places to check before committing. Kernel sharing means all containers run the kernel the host ships, so containers that need exotic kernel features or that require distinct kernel versions are VM cases. The host kernel upgrade reboots every container at once, so maintenance windows are fleet-wide by construction. And the escape risk, small and shrinking, is managed rather than eliminated, unprivileged by default, the hardening stack from this series on the host, and the threat model documented honestly as separation of services rather than defense against the outside world.

The rollout checklist follows the article's order: install LXC, create one privileged-examined and unprivileged-defaulted test container, graduate networking to the bridge model, apply the cgroup budgets per tenant, wire backups on the storage tier chosen, clone for staging, and place the whole set under the host's hardened perimeter. The result is a dedicated server that behaves like a small fleet, administered like one machine, with the isolation story told in one sentence, every tenant is a directory and a config file, and the sentence is true, auditable and the entire architecture.
## Autostart, systemd and the boot-order contract

Containers that must survive reboots need their start behavior declared rather than remembered, and LXC carries two lines for it in the container's config:

lxc.start.auto = 1
lxc.start.delay = 5

The first line tells LXC's boot machinery to start the container when the host boots, and the second spaces the startups out, five seconds between launches, which matters on a box with a dozen containers all hitting their disks at once. The ordering contract goes one step further with groups and priorities, lxc.start.order assigning sequence numbers so the database container boots before the application containers that depend on it, a miniature dependency graph expressed in two configuration lines per container.

On a systemd host, LXC services integrate natively: lxc-monitord runs as the supervision layer, and the per-container unit, lxc@<name>.service, lets systemctl manage individual containers with the same vocabulary as any other service, restart policies, journal logging and dependencies included. The unit composes with the watchdog patterns from earlier articles, a container that must never stay down pairs lxc@web01 with Restart=always, and the boot contract ends with every tenant back in its cage within a minute of power-on, in the order the config file promised.

The observation commands complete the daily picture: lxc-info -n web01 prints the container's state, its process count, its memory use and its IP in one block, and lxc-top is the live dashboard across all containers, the two commands covering the morning check on a small fleet from a single SSH session.