Firecracker vs LXC/LXD: microVMs vs system containers
LXC and LXD are the containers that feel the most like virtual machines. Unlike a single-process Docker container, an LXD instance boots a full init system, runs its own syslog and cron, gets its own IP, and lets you `apt install` and `systemctl` exactly as you would on a real box. It's convincing enough that people reach for it to run other people's workloads. So the honest question is: is that VM feeling actually VM-grade isolation, or just a very good costume? The short answer — it's a costume, and knowing where the seams are tells you exactly when LXD is fine and when you need a real microVM like Firecracker instead.
What LXC/LXD actually are: system containers
LXC (Linux Containers) is the low-level toolkit; LXD (now maintained as Incus in the community fork, with Canonical continuing LXD) is the friendlier daemon and CLI layered on top. Both create what's called a system container — a container that runs a full OS userland (an init, services, package manager) rather than one foreground process. But mechanically it's the exact same set of Linux primitives Docker uses: namespaces decide what the container can see (its own PIDs, mounts, network, hostname), cgroups decide what it can use (CPU, memory, I/O), and AppArmor/seccomp/capabilities narrow what it's allowed to do to the kernel.
The critical fact underneath all of that: there is exactly one kernel. Every LXC/LXD container on a host runs on the host's kernel. The container has no kernel of its own — when a process inside it makes a syscall, that syscall is serviced by the same kernel serving every other container and the host itself. That's why system containers are so cheap and boot so fast: there's no second OS to start, just namespaces being created around processes that were always host processes.
But LXD has unprivileged containers — isn't that solved?
This is the objection worth taking seriously, because unprivileged containers are genuinely good engineering. In an unprivileged LXD container, root inside the container (UID 0) is mapped via a user namespace to some high, harmless UID on the host (say 1000000). So even if a process breaks out of its mount or PID namespace, it lands on the host as a nobody, not as real root. Combine that with a restrictive AppArmor profile and a seccomp filter that blocks the scariest syscalls, and you've meaningfully raised the bar. LXD does this by default, and it's the right default.
Here's the catch. Every one of those defenses is a policy layer sitting on top of the same shared kernel — and the shared kernel is still the thing executing the untrusted code's syscalls. User namespaces themselves have been a rich source of privilege-escalation CVEs (they expose kernel code paths that were previously root-only to unprivileged users). seccomp filters block syscalls someone thought to block. A kernel bug in a syscall you didn't filter, reachable from an unmapped-but-still-present code path, doesn't care that your UID is 1000000. The Linux syscall surface is enormous, and it is the attack surface — you are betting that no exploitable bug exists in the parts of it your container can reach.
An unprivileged container is a container that hasn't found the right CVE yet. It's a much better bet than a privileged one — but it's still a bet against the same kernel.
How Firecracker moves the boundary
A Firecracker microVM is a different category of thing. It's a real hardware-virtualized guest running under KVM, with its own guest kernel booted from its own kernel image. Untrusted code inside the guest makes syscalls to the guest kernel — a kernel that is not your host's kernel and shares no state with it. The guest can only reach the outside world through a deliberately tiny set of emulated virtio devices (a block device, a network device, vsock) exposed by the Firecracker VMM, which itself runs as an unprivileged host process behind seccomp and, in production, the jailer.
So to escape, untrusted code has to first find a bug in the guest kernel (which only gets it root inside a throwaway VM — nowhere), then find and exploit a bug in Firecracker's minimal device-emulation surface or the KVM/hardware virtualization boundary beneath it. That's a vastly smaller and more heavily audited surface than the full Linux syscall interface an LXC container is exposed to. It's the same reason AWS Lambda and Fargate run every customer's code in Firecracker microVMs rather than in shared-kernel containers: at multi-tenant scale, the shared kernel is not a boundary you're willing to bet strangers' code against.
Side by side
- Isolation model — Firecracker: hardware-virtualized microVM, own guest kernel under KVM. LXC/LXD: namespaces + cgroups + AppArmor/seccomp on the shared host kernel.
- Kernel — Firecracker: separate guest kernel per VM (pick your own version). LXC/LXD: the host kernel, shared by every container; you cannot run a different kernel.
- Untrusted / AI-generated / multi-tenant code — Firecracker: designed for it (the AWS Lambda model). LXC/LXD: risky even unprivileged — a kernel bug escapes the box.
- What it feels like — Firecracker: a small, fast VM. LXC/LXD: a lightweight VM, but it isn't one — same kernel underneath.
- Boot / create — Firecracker: cold boot in the low seconds; snapshot-restore makes per-request creates practical. LXC/LXD: very fast (no OS to boot, just namespaces).
- Density / overhead — Firecracker: a few MB per guest for kernel + device model. LXC/LXD: near-zero beyond the container's own processes.
- Best fit — Firecracker: multi-tenant, untrusted, per-user, or model-generated workloads. LXC/LXD: trusted multi-workload hosting, homelabs, dev boxes, CI you control.
When LXC/LXD is genuinely the right tool
None of this makes LXD bad — it makes it a hosting tool, not an untrusted-code sandbox. If you control everything running inside the containers, the shared-kernel risk is your own risk, and LXD's density, speed, and full-OS ergonomics are hard to beat. Reach for it happily when:
- You're carving one beefy server into several trusted workloads — a Postgres box, an app server, a monitoring stack — all run by you or your team.
- Homelab and self-hosting: running a dozen services on one machine where every container is something you chose and configured.
- Dev and test environments: a full-OS box per project or per branch that developers can SSH into and treat like a real machine.
- CI runners for your own repos, where the code being built is first-party and the threat model is accidents, not adversaries.
- You specifically need a full systemd userland (services, timers, journald) that an application container makes awkward.
Two examples that look similar and aren't: 'run five of my own services on one VPS' is a great LXD job. 'Let strangers upload code and run it on my host' is not — no matter how carefully you configure the AppArmor profile.
When you must reach for a microVM
The line is trust, not workload size. The moment the code running inside is untrusted, multi-tenant, or generated at runtime, a shared kernel stops being an acceptable boundary and you want a real VM boundary. Concretely:
- AI agents executing model-generated shell commands or code — you can't review it before it runs, and prompt injection turns 'analyze this file' into 'exfiltrate the environment'.
- Multi-tenant platforms where different customers' code lands on shared hosts — one tenant's kernel exploit must not reach another's data.
- Per-user code playgrounds, online judges, and code interpreters that run arbitrary submissions.
- Running untrusted or unaudited third-party plugins, scrapers, or build steps from repos you don't control.
Here's the same 'launch a box and run something in it' operation in both worlds. First, LXD — an unprivileged Ubuntu system container you'd use for a trusted workload:
# LXD: a full-OS system container — great for a workload YOU control.
lxc launch ubuntu:24.04 my-service
lxc exec my-service -- apt-get update
lxc exec my-service -- systemctl status
# Runs on the HOST kernel. `uname -r` inside == the host's kernel.
# Unprivileged by default, but a kernel bug here still reaches the host.And the same shape for untrusted code, where each run gets its own guest kernel behind a hardware boundary. This is exactly the line PandaStack is built on — every sandbox is its own Firecracker microVM, and because a create restores a baked snapshot on demand rather than cold-booting, the VM-grade isolation costs you almost nothing in latency (create is p50 ~179ms):
from pandastack import Sandbox
# Firecracker microVM: separate guest kernel, hardware-isolated.
# The right shape for untrusted / model-generated code.
with Sandbox.create(template="code-interpreter", ttl_seconds=300) as sbx:
result = sbx.exec("python3 -c 'import os; print(os.uname().release)'",
timeout_seconds=30)
print("guest kernel:", result.stdout.strip()) # NOT your host's kernel
print("exit:", result.exit_code)
# VM is destroyed here — blast radius of arbitrary code is one throwaway guest.Same ergonomics, different boundary. With LXD the blast radius of a kernel exploit is your host and every neighbor; with a per-run microVM it's a disposable guest with its own kernel, memory, filesystem, and network namespace. Historically that isolation meant paying multi-second VM boot times per request, which is why people stretched containers to cover jobs they shouldn't. Snapshot-restore removes that excuse: creating a fresh microVM per untrusted run is now fast enough to do on every request.
The honest verdict
LXC/LXD and Firecracker aren't really rivals — they answer different questions. If your question is 'how do I run several workloads I trust efficiently on one machine,' LXD is an excellent answer and a microVM would be overkill. If your question is 'can this safely run code I don't control,' a shared-kernel container — unprivileged or not — says 'probably, until it doesn't,' and a hardware-isolated microVM says 'yes.' Pick the boundary that matches your trust model, and verify the specifics against each project's current docs, since kernel-hardening features and defaults move over time.
Frequently asked questions
Is an LXC/LXD container a virtual machine?
No. LXC/LXD create system containers — they run a full OS userland (init, services, package manager) so they feel like a lightweight VM, but they share the host's kernel through namespaces and cgroups. There is no guest kernel and no hardware virtualization boundary. A virtual machine like a Firecracker microVM boots its own separate guest kernel under KVM; that's the difference that matters for isolation.
Are unprivileged LXD containers safe enough to run untrusted code?
Unprivileged containers meaningfully raise the bar — root inside maps to an unprivileged host UID, and AppArmor/seccomp narrow the syscall surface — but they still run untrusted code against the shared host kernel. A single exploitable kernel bug in a reachable syscall path escapes the container regardless of UID mapping. For genuinely untrusted, multi-tenant, or AI-generated code, use a hardware-isolated microVM (Firecracker) so a kernel bug only compromises a throwaway guest, not the host.
Why do platforms like AWS Lambda use Firecracker instead of LXC/LXD?
Lambda runs untrusted code from many customers on shared fleets, so tenant-to-tenant isolation has to hold even against a kernel exploit. LXC/LXD share one host kernel across all containers, so a kernel bug crosses tenant boundaries. Firecracker gives each workload its own guest kernel behind a hardware boundary while still booting in milliseconds — the isolation Lambda needs without full-VM startup cost.
When should I choose LXD over a microVM?
Choose LXD when you control the code and want to run several trusted workloads efficiently on one host: homelabs, dev and test boxes, first-party CI runners, or splitting a server into your own service containers. Its density, speed, and full-OS ergonomics are hard to beat there. Reach for a microVM the moment the code is untrusted, multi-tenant, or generated at runtime, where a shared kernel is no longer an acceptable security boundary.
What's the difference between LXC, LXD, and Incus?
LXC is the low-level Linux container toolkit; LXD is the higher-level daemon, API, and CLI built on top of it for managing system containers (and VMs) more ergonomically. Incus is the community fork of LXD maintained under the Linux Containers project, while Canonical continues to develop LXD. All of them create shared-kernel system containers — the container-vs-microVM isolation distinction applies to every one of them. Check current project docs for feature and default differences.
49ms p50 cold start. Fork, snapshot, and scale to zero.