The Container That Forgot It Was a VM
You’ve got Proxmox running at home. You want to spin up a dev environment. Your buddy says “just Docker it.” You say “I’m pretty sure I need systemd running in here.” They look confused. You look confused. Welcome to the gap between LXC and Docker — two things called “containers” that are solving entirely different problems.
Here’s the thing: Docker and LXC are cousins, not twins. They both use Linux namespaces and cgroups under the hood, but they’re pointing in opposite directions. Docker is all about packaging a single application into an immutable artifact that runs the same way everywhere. LXC is about running a lightweight, persistent operating system that just happens to share a kernel with your host.
One is a shipping container full of identical cardboard boxes. The other is a studio apartment with running water and electricity. Both are “containers.” Only one makes sense when you need a full OS.
What’s Actually Different? (Beyond the Marketing)
Let’s demystify the machinery. Both technologies share the same Linux kernel isolation tools — namespaces and cgroups. The difference is how they’re stacked.
Docker: The Application Container
Docker wraps a single application (Nginx, Redis, your Python service) inside a layered filesystem with its own minimal OS (Alpine, Debian slim, or busybox). When you run a Docker container, you get:
- One main process (PID 1)
- No init system (the process IS the container lifecycle)
- Ephemeral by design (stop it, data’s gone unless you mounted a volume)
- Layered images (changes on top of a base)
- A registry ecosystem (Docker Hub, registries, easy distribution)
Docker’s philosophy: immutability. Rebuild and redeploy instead of patching. Perfect for stateless microservices and CI/CD pipelines.
LXC: The System Container
LXC runs a full Linux distribution inside the container. You get:
- init process (systemd or OpenRC)
- Multiple services (Nginx, PostgreSQL, Redis, all running together)
- Persistent filesystem (changes survive a reboot)
- Bind mounts to the host (direct access to host directories, no copy-on-write layers)
- SSH/login capability (you can
sshinto your container like a lightweight VM)
LXC’s philosophy: lightweight virtualization. It’s a VM that doesn’t need its own kernel.
The mental model: Docker is “package my app”, LXC is “give me a minimal OS to run stuff on.”
Kernel Sharing: The Secret Sauce
Both use the same kernel. Your host Linux kernel runs all containers. This is why Docker on a Mac or Windows feels janky — they’re running a hidden Linux VM, and the Docker containers run inside that VM.
LXD (the modern “Linux Container Daemon” — LXC’s successor/management layer) and Incus (the Canonical-backed fork) make LXC containers easier to manage. They abstract away the config complexity. When you say “run an LXC container,” you’re usually invoking LXD or Incus, not raw LXC.
The shared kernel means:
- LXC containers overhead: mostly filesystem + namespace setup
- Docker containers overhead: same, plus the image layers and union filesystem logic
- Both are vastly lighter than a full VM with its own kernel
On a modern system, a running LXC container might use 30-100 MB of RAM just sitting there. A VM starts at 512 MB–2 GB, plus whatever the bootloader and OS demand. That’s the gap.
When LXC Wins (And It’s Your Homelab)
You’re running Proxmox. You want to spin up a Postgres database, an Nginx reverse proxy, and a background job queue. You could:
Option A (Docker): Three separate containers, a bridge network, volumes for persistence, maybe Docker Compose orchestration. Very clean, very modern, very “one process per container.”
Option B (LXC): One container with a full Ubuntu install, systemd managing all three services, a single bind mount to your host data directory, and you just lxc exec container -- apt install postgresql nginx or configure it with Ansible. The whole thing boots in 2 seconds.
LXC wins here because:
- Multi-process workloads feel natural. You’re not fighting the “one process per container” constraint.
- Bind mounts are simpler. Instead of Docker volumes with permission headaches, you just mount
/mnt/datadirectly to the host. No copy-on-write overhead. - Init system integration. systemd is running. Cron jobs work. Service restarts are handled by systemd, not orchestration tooling.
- Persistent root filesystem. Your container survives reboots. You can SSH into it, debug, tweak configs, and not worry about drifting from your Compose file.
- Legacy software. Got a hairy old application that needs running as a proper service with environment variables, signal handling, and a process group? LXC runs it. Docker makes you fight the “run as PID 1” weirdness.
Concrete LXC Example
Here’s a Proxmox LXC container running Postgres + a simple Nginx proxy:
# Create container (via Proxmox UI or CLI)lxc launch ubuntu:22.04 myapp
# Enter itlxc shell myapp
# Inside the container:apt update && apt install -y postgresql postgresql-contrib nginx
# Start services (systemd runs them automatically on reboot)systemctl restart postgresqlsystemctl restart nginx
# Bind mount your data directory from the host# (done at container creation or via `lxc config device add`)
# And you're done. SSH in whenever you want.ssh root@<container-ip>That’s it. No Compose file. No volume mounts. No networking plumbing. You’ve got a lightweight OS.
When Docker Wins (Most Deployments)
Docker dominates because:
- Immutable deploys. Rebuild the image, push it, deploy it. No drift. Every environment is identical.
- Image registry ecosystem. Docker Hub, GitHub Container Registry, private registries. Sharing is baked in.
- Stateless by design. Forces you to externalize state (databases, caches, object storage). Better architecture.
- Orchestration maturity. Docker Swarm, Kubernetes, Nomad. Tons of tooling to manage many containers.
- Single responsibility. One process per container. Easy to reason about, easy to scale horizontally.
Concrete Docker Example
Same workload, Docker style:
version: '3.8'services: postgres: image: postgres:15-alpine environment: POSTGRES_PASSWORD: insecure123 volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432"
nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - postgres
volumes: pgdata:Then:
docker compose up -dYou’ve got the same services, isolated, version-controlled in a single file, deployable anywhere Docker runs. Trade-off: each service is a separate container, networking is explicit, volumes require thinking about lifecycle.
The LXD / Incus Situation
LXD (made by Canonical) was the modern way to manage LXC containers — better CLI, snapshots, proper networking, profiles. Then Canonical moved LXD to a proprietary model, and the community forked it as Incus. Most people building new stuff use Incus now.
Both work the same way for your purposes. On Proxmox, you’re using LXC directly (Proxmox has native LXC support), so this is mostly academic unless you’re on Ubuntu or using standalone LXD/Incus.
Security: The Asterisk
Docker containers are application sandboxes. Each container runs one app with minimal OS.
LXC containers are lightweight VMs. They run a full OS with root and everything. That’s powerful but also means:
- Root inside the container is root (without proper user namespace remapping)
- A root escape inside the container can be bad
- You need to configure user namespaces if security is tight
For a home lab, this is fine. For production or multi-tenant, you’d want proper user namespace remapping and security profiles.
Docker pushes security as a feature: “you can run untrusted code safely.” LXC is more: “it’s a lightweight VM, secure it like you’d secure a VM.” Different threat models.
Resource Reality Check
Here’s the math. On a 16 GB system:
- 10 Docker containers (simple services): ~2-3 GB total overhead
- 10 LXC containers (full OS): ~2-4 GB total overhead
- 10 full VMs (Proxmox/KVM): ~15-20 GB total overhead
The difference between Docker and LXC containers at scale is small. The difference between containers and VMs is massive.
For a single persistent workload (Postgres, a background queue, your media server), LXC’s slightly smaller overhead doesn’t matter. The convenience of a single init-managed system matters more.
When to Pick What
Here’s your decision tree:
Use Docker if:
- Building a microservice (one app, one responsibility)
- Deploying to cloud or Kubernetes
- You want immutable, version-controlled deployments
- Multiple environments (dev/staging/prod) need to be identical
- You’re scaling horizontally (many copies of the same service)
Use LXC if:
- Running a persistent, multi-service workload
- You want a “lightweight VM” you SSH into and manage like a traditional server
- The workload has multiple processes (Postgres + Nginx + cron jobs)
- You’re on Proxmox or a home lab and want simplicity
- You need systemd, proper init, service management
- You’re running legacy software that expects a real OS
- Direct bind mounts to host data feel natural
Use both if:
- Run the OS (LXC) and containerize services inside it (Docker). Honestly, this is overkill for most home labs, but it works.
The Honest Take
Docker is the industry standard because it solved a real problem: immutable deployments and dev/prod parity. It’s excellent at what it does.
LXC is ignored in most DevOps conversations because it doesn’t fit the “stateless microservice” model. But for a home lab, a single server, or a workload that needs persistent state and systemd? LXC is simpler and more natural.
The mistake is thinking you have to pick one. Docker and VMs both exist. LXC is the bridge — lightweight virtualization when containers feel too minimal and VMs feel too heavy.
Your Proxmox box can run both LXC containers (for long-lived stuff) and Docker (if you spin up an Ubuntu container and install Docker in it, though that’s a bit meta). Different tools, different contexts, different problems.
That’s the real distinction. Docker and LXC aren’t fighting for the same job. Docker is asking “how do I package and deploy applications?” LXC is asking “how do I run a bunch of stuff on minimal hardware without booting full VMs?”
Both valid questions. Now you know which one to ask first.