LXC vs Docker: System Containers vs Application Containers for a Private DNS Stack
The short answer
Docker and LXC are both container technologies, and both share your host's kernel. The difference is the philosophy:
- Docker runs an application. You pull an image, it starts one program, and the container is disposable. Your configuration lives in a compose file and a few volumes.
- LXC runs a small machine. You get a minimal Debian or Ubuntu that boots with its own init system, has its own IP address, and that you log into, install packages in, and snapshot like a tiny server.
For a DNS filter, which is a small, always-on, network-facing service, both work well. A rough rule of thumb:
- Choose Docker if you like declarative, reproducible setups, you already run other Compose stacks, and you want updates to be "pull a new image and recreate".
- Choose LXC if you want the DNS server to behave like a normal, separate machine on your LAN with its own IP, its own port 53, and normal tools such as
apt,systemd,journalctland SSH.
The rest of this guide explains why, with the DNS-specific details that tend to cause trouble.
What a container actually is
A container is not a small virtual machine. There is no second kernel. A container is an ordinary process (or group of processes) on the host that the kernel has been told to isolate, using two main features:
- Namespaces give a process its own view of the system: its own process IDs, network interfaces, mount points, hostname and user IDs. A process inside a container thinks it is alone on the machine.
- Control groups (cgroups) limit how much CPU, memory, disk I/O and process count the group can use.
Because the kernel is shared, containers start in about a second, use very little extra memory, and run at essentially native speed. The trade-off is that isolation is weaker than a VM's: a serious kernel vulnerability can affect every container on the host. We come back to this in the security section.
Both Docker and LXC are built on these same kernel features. They differ in what they put inside the isolated space and in how you are expected to manage it.
Docker: the application container
Docker packages one application plus its dependencies into an image. Images are built in read-only layers, which makes them easy to share and cache. When you run an image, Docker adds a thin writable layer on top and starts a single main process.
The key ideas:
- One process per container. A container for Pi-hole, a separate one for Unbound, a separate one for a reverse proxy. There is usually no init system, no SSH and no package manager you are expected to use at runtime.
- Ephemeral by design. The container's own filesystem is throwaway. Anything you want to keep (Pi-hole's
/etc/pihole, your Unbound config) must be in a volume or bind mount. Delete and recreate the container and everything else is gone. - Declarative configuration. A
compose.yamlfile describes the whole stack: images, ports, volumes, environment variables, restart policy. You can keep it in Git and rebuild the whole thing on a new host in minutes. - A large ecosystem. Official and community images exist for almost everything, including Pi-hole itself.
Docker in practice for DNS
A minimal example, Pi-hole with Unbound as its upstream (adjust image names, tags and variables to the current documentation before using):
services:
unbound:
image: mvance/unbound:latest
restart: unless-stopped
networks:
dns:
ipv4_address: 172.20.0.2
pihole:
image: pihole/pihole:latest
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: "Europe/London"
FTLCONF_webserver_api_password: "change-me"
FTLCONF_dns_upstreams: "172.20.0.2#53"
FTLCONF_dns_listeningMode: "all"
volumes:
- ./etc-pihole:/etc/pihole
depends_on:
- unbound
networks:
dns:
ipv4_address: 172.20.0.3
networks:
dns:
ipam:
config:
- subnet: 172.20.0.0/24
Things worth noticing:
- Unbound gets a fixed IP on the Compose network, because Pi-hole's upstream setting expects an IP address rather than a container name.
- The Pi-hole data lives in
./etc-pihole, outside the container, so recreating the container does not lose your lists or settings. - Upgrading is
docker compose pull && docker compose up -d.
LXC: the system container
LXC (Linux Containers) takes the opposite approach. A container is a full userspace: a minimal Debian, Ubuntu, Alpine or similar, with a real init system, its own users, its own package manager, and its own network stack. You start it, log in, and it feels like a small server.
The key ideas:
- It behaves like a machine.
systemdruns inside,aptworks, services start on boot,journalctlshows logs, and you can SSH in. - Persistent by default. The container's root filesystem is its own storage. There is no separate "volumes" concept you must remember. If you install something, it stays.
- Imperative configuration. You set it up by running commands inside it, the same way you would on a VM or Raspberry Pi. That is quick and familiar, but it is not self-documenting. Your configuration lives in the container's state unless you script it yourself.
- Typically unprivileged. Modern LXC maps the container's root user to an unprivileged user on the host (using user namespaces), which greatly limits the damage a compromised container can do.
LXC in practice for DNS
With the classic LXC tools, creating a Debian container, giving it a LAN address through a bridge, and installing Pi-hole looks roughly like this:
# Create a Debian container from the public image server
lxc-create -n dns -t download -- -d debian -r stable -a amd64
# In the container's config (e.g. /var/lib/lxc/dns/config), attach it to your LAN bridge
# lxc.net.0.type = veth
# lxc.net.0.link = br0
# lxc.net.0.flags = up
lxc-start -n dns
lxc-attach -n dns
# Inside the container: set a static IP, then install normally
curl -sSL https://install.pi-hole.net | bash
Many people manage LXC through a higher-level tool or web interface that wraps these steps (Proxmox, for example, has its own container tooling). The concepts are the same: the DNS server is a separate, persistent mini-machine with its own address on your network.
Pi-hole's official installer was designed for exactly this kind of environment (a normal Linux system you can install packages into), which is part of why LXC feels natural for it.
Side-by-side comparison
| Docker | LXC | |
|---|---|---|
| Model | Application container | System container (mini machine) |
| Processes | One main process per container | Full init system, many services |
| Filesystem | Layered image plus throwaway layer; keep data in volumes | Normal persistent root filesystem |
| Configuration style | Declarative (compose file, environment variables) | Imperative (commands, config files inside) |
| Reproducibility | Excellent: rebuild from a file | Needs scripting or backups to reproduce |
| Updates | Pull a new image, recreate | apt upgrade and pihole -up inside |
| Rollback | Pin the previous image tag, restore the volume | Snapshot and restore the whole container |
| Networking default | NAT behind the host, ports published | Often bridged: own IP on your LAN |
| Port 53 conflicts | Can collide with host services (e.g. systemd-resolved) | Own network namespace, no collision |
| Debugging | docker logs, docker exec |
Normal tools: journalctl, SSH, systemctl |
| Security isolation | Shared kernel, namespaces and capabilities | Shared kernel, plus user-namespace separation when unprivileged |
| Learning curve | Concepts to learn (images, volumes, networks) | Feels like administering a small server |
| Best at | Repeatable stacks, many small services | Long-lived "pet" services that act like machines |
Networking: where DNS servers get tricky
A DNS server is unusually sensitive to networking details, because every device on your LAN needs to reach it on port 53, and Pi-hole wants to know which client sent each query. This is where the two models differ most.
Docker networking
With Docker's default bridge network, the container lives on a private subnet behind the host, and you publish ports (53:53) so traffic on the host's IP is forwarded in. Things to watch for:
- Port 53 may already be taken. On many Ubuntu systems,
systemd-resolvedruns a stub listener on port 53 of the loopback address. Publishing port 53 can fail or behave oddly until you disable that stub listener or bind to a specific host IP. - Client visibility. In some bridge-mode setups, queries can appear to come from the Docker gateway address rather than the real device, so your per-device statistics become useless. If you see every query attributed to one client, this is the first thing to check.
- Host networking.
network_mode: hostremoves the isolation: the container uses the host's network directly. Client IPs are preserved and port 53 is simply the host's port 53, but you lose port separation and must manage conflicts yourself. - Macvlan. A macvlan network gives the container its own IP on your LAN, which is closest to the LXC experience. The classic catch is that the host itself cannot talk to its macvlan containers without extra setup (a macvlan shim interface).
- Firewall surprises. Docker writes its own firewall rules for published ports. On hosts that use a simple frontend firewall, a published port can be reachable even when you thought it was blocked. For a DNS server this matters: make sure you are not accidentally running an open resolver that answers queries from the internet.
LXC networking
The most common LXC setup attaches the container to a bridge on your LAN. The container then has its own MAC address and IP, just like a Raspberry Pi would. For DNS this is very convenient:
- Port 53 (and 80, 443, 853) belong to the container alone. Nothing on the host competes.
- Clients are seen with their real addresses.
- You point your router's DHCP at the container's IP and you are done.
- You can run a separate container per role, each with its own address, such as one for Pi-hole and one for an encrypted DNS front end.
The usual caveats are those of any bridged setup: Wi-Fi interfaces generally cannot be bridged this way, so the host should be on wired Ethernet, and you should give the container a static or reserved address, since your whole network depends on it.
Encrypted DNS ports
If you serve DoT (port 853) or DoH (port 443) yourself, you need a front end that terminates TLS (a reverse proxy or a DNS-aware proxy) and certificates that renew automatically. Both models can do this. In Docker it is another service in the compose file. In LXC it is another package or a second container. Either way, make sure the certificate renewal path is monitored, because an expired certificate will make encrypted clients fail to connect. Our guide on encrypted DNS on every device covers the client side.
Updates, backups and rollback
Docker
Because the container is disposable, the question is "what must I keep?" The answer is small: your compose file and your volumes.
- Update:
docker compose pull && docker compose up -d. - Roll back: pin the previous image tag (or digest) in the compose file and recreate. Avoid
latestfor anything you depend on, so that an update is something you choose and not something that happens to you. - Back up: copy the compose file and the Pi-hole and Unbound data directories. Pi-hole's built-in Teleporter export is a good extra safety net.
- Gotcha: a major version jump (for example Pi-hole v5 to v6) may change environment variable names and file layout. Read the release notes before pulling, and test with a copy of your data.
LXC
The container is a persistent filesystem, so you protect the whole thing.
- Update:
apt update && apt upgradeinside, pluspihole -upfor Pi-hole. Consider unattended security updates for the base OS. - Roll back: take a snapshot before any change. On storage that supports it (ZFS, Btrfs, LVM-thin), snapshots are instant and cheap, and restoring one returns the entire machine to its earlier state, including the OS, the package versions and the data.
- Back up: export or copy the container's storage, or back it up with whatever backup tool manages your host. Because the whole machine is in one place, restoring is simple.
- Gotcha: nothing documents what you changed. If you rebuild from scratch in a year, you will not remember every tweak, so keep notes or a short setup script.
The honest summary: Docker is better at reproducing a setup, and LXC snapshots are better at undoing a change to a running setup.
Security and isolation
Neither is a security boundary on the level of a virtual machine, because the kernel is shared. A kernel vulnerability that allows container escape can affect both. That said, the details matter.
Docker
- By default the Docker daemon runs as root, and anyone who can talk to the Docker socket effectively has root on the host. Treat that socket as sensitive.
- Containers drop most Linux capabilities by default. Give a DNS container only what it needs (binding to port 53 and, if you use Pi-hole's DHCP server, network administration). Check the image documentation for the current capability list instead of copying old compose snippets.
- Rootless mode exists and reduces the blast radius, but binding to low ports and some networking options become more awkward.
LXC
- Unprivileged containers map the container's root user to a harmless high-numbered user on the host. A process that "escapes" as root inside the container is still an ordinary unprivileged user outside it. This is a meaningful hardening step and a good default.
- Privileged containers do not have that mapping and are noticeably weaker. Avoid them unless something genuinely requires it.
- Because a container has a full OS, it also has a full OS attack surface: SSH, cron, extra packages. Keep it minimal, patch it, and do not expose anything you do not need.
For both models, on a DNS server:
- Do not expose port 53 to the internet. An open resolver gets abused for amplification attacks quickly.
- Keep the admin interface on your LAN or behind a VPN.
- Keep the host patched, since the host kernel protects everything above it.
- If you want strong isolation between services, use a VM for that, not a container.
Resources and performance
Both are close to bare metal for CPU and network throughput, and a DNS filter is a light workload anyway. The practical differences are small:
- A Docker container running just Pi-hole carries only that application's footprint.
- An LXC container running a full Debian with
systemdcarries a little more baseline memory, because it runs a handful of system services alongside Pi-hole. On any machine capable of running containers at all, this difference is tiny. - Starting, stopping and snapshotting are fast on both.
Where performance actually matters for DNS is cache size, upstream latency and disk I/O for the query log, not the container technology. If you are tuning, look at Unbound's cache settings and your log retention first. Our guide on what happens in the 50 ms before every request shows where the time really goes, and the Unbound guide covers tuning.
Which should you choose?
Choose Docker if:
- You already run Docker or Compose for other services.
- You want your whole DNS stack described in one file you can store in Git and redeploy anywhere.
- You like pinning versions and rolling back by changing a tag.
- You are comfortable with a few networking concepts (published ports, macvlan or host networking).
Choose LXC if:
- You want the DNS server to be its own machine on the LAN, with its own IP and its own port 53, and no port juggling.
- You prefer standard Linux administration:
apt,systemd, logs, SSH. - You value instant snapshots before risky changes.
- You are following a tutorial written for a normal Debian or Ubuntu machine, and want it to work as written.
Choose neither (a VM or dedicated hardware) if:
- This DNS server protects a household or business where an outage or a compromise is costly, and you want the strongest isolation. A small VM or a dedicated Raspberry Pi is the conservative choice. Our Raspberry Pi guide covers that route.
Not sure? For a first Pi-hole on a home network, an LXC container is usually the gentlest start because everything behaves like the guides you will find. Once you are comfortable and want reproducibility, moving to Docker Compose is a reasonable next step. Since your data (blocklists, custom DNS records, allow and deny lists) can be exported with Teleporter, migrating between the two is not painful.
Common mistakes and hybrid setups
Mistakes we see often
- No static address. A DNS server whose IP changes silently breaks the whole network. Reserve it in your router or set it statically.
- One DNS server, no fallback. If it is down, "the internet is broken" for everyone. Consider a second instance, or at least a sensible fallback for non-critical devices.
- Using
latesttags in Docker and being surprised by a major upgrade. - No snapshot before an LXC upgrade, then no way back.
- Forgetting persistent storage in Docker, so recreating the container wipes your configuration.
- Publishing port 53 on a public interface and creating an open resolver.
- Running a privileged LXC container because a tutorial said to, without needing it.
- Letting certificates expire on an encrypted DNS endpoint.
Hybrid approaches
- LXC container hosting Docker. Many people run a Docker host inside an LXC container to get snapshots of the whole Docker environment. It works, but it needs nesting enabled and some extra container permissions, and it layers one isolation mechanism on another, which can complicate storage drivers, networking and security. A small VM is the cleaner choice for hosting Docker.
- Split roles. One LXC container for Pi-hole and Unbound (the critical, boring part), Docker for everything else.
FAQ
Is LXC the same as Docker? No. They share the same kernel features, but Docker is designed to package and run single applications, while LXC is designed to run a whole lightweight operating system.
Can I run Pi-hole in either one? Yes. Pi-hole publishes an official Docker image, and its installer works on standard Linux systems, which includes an LXC container.
Which is more secure? Neither wins outright. An unprivileged LXC container and a rootless Docker setup are both reasonable. What matters more is not exposing port 53 publicly, keeping the host patched, and giving each container only the permissions it needs.
Which uses less memory? Docker typically has a slightly smaller footprint per service. For a DNS filter, the difference is not usually important.
Which is easier to back up? LXC, if your storage supports snapshots: the whole machine is one unit. Docker is easy too, as long as you remember to back up your volumes and compose file.
Do I need a VM instead? Only if you want stronger isolation than a shared kernel gives you, or you need to run a different operating system.
Can I switch later? Yes. Export your settings with Pi-hole's Teleporter and your Unbound configuration file, then import them into the new environment. Plan a short maintenance window, and keep the old instance until the new one has proven itself.