I’m sitting here looking at a dashboard full of “microservices” consuming 64GB of RAM to serve a basic CRUD application that could have run on a 486 DX2 with 8MB of memory, and I’m being asked to explain what is docker to a generation of developers who think a “server” is an abstract concept living in a cloud.
Fine. You want to know what it is? It’s not a revolution. It’s not a “game-changer”—god, I hate that phrase. It’s a glorified wrapper around Linux kernel features we’ve had since the early 2000s, packaged for people who can’t be bothered to learn how a linker works. It’s a way to ship your “it works on my machine” excuses in a neat little tarball so that when it breaks at 3:00 AM, it breaks in a predictable, isolated way that I still have to fix.
Table of Contents
1. The 3:00 AM Incident: Why This Mess Exists
Before I dissect the guts of Docker Engine v24.0.7, you need to understand the trauma that birthed it. It’s 2004. I’m in a data center in Northern Virginia. The AC is failing, the floor tiles are vibrating, and a Sun Fire V240 is screaming because a “senior” dev pushed a binary compiled against a version of glibc that didn’t exist on the production cluster.
Back then, we didn’t have “containers.” We had bare metal. If you wanted isolation, you bought another $10,000 rack-mounted heater. We spent six hours that night trying to manually symlink shared libraries, fighting dependency hell while the database sat locked because some “helpful” script had overwritten a global environment variable. We were basically performing open-heart surgery on a running OS with a rusty spoon.
Docker exists because you lot kept breaking the global namespace. You couldn’t keep your binaries in your pants, so the industry had to build a playpen. When people ask “what is docker,” the honest answer is: it’s a straitjacket for software.
# This is what my nightmare looked like before you kids got your 'containers'
# Just trying to see why the hell the linker is crying
ldd /usr/local/bin/legacy_app
linux-vdso.so.1 (0x00007ffcb95f3000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f8e1a200000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e19e00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8e1a5f5000)
# Oh look, a version mismatch. Time to spend 4 hours rebuilding the world.
2. The Kernel Lie: Namespaces and the Illusion of Isolation
Let’s get one thing straight: Docker is not a Virtual Machine. If I hear one more “DevOps Evangelist” compare a container to a VM, I’m going to throw my Model M keyboard at them.
A VM uses a hypervisor to emulate hardware. It’s heavy, it’s slow, but it’s honest. Docker is a liar. It uses the same host kernel as everyone else. When you run a container, you’re just running a process on the host, but the kernel is wearing a blindfold.
Docker relies on two primary Linux primitives: Namespaces and Control Groups (cgroups).
When you run docker run, the engine calls clone() with a bunch of flags like CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWNS, and CLONE_NEWUTS.
– CLONE_NEWPID tells the process: “You are PID 1. You are the king of the world.” In reality, on the host, it’s just PID 45029.
– CLONE_NEWNET gives it a private network stack so it doesn’t see the host’s interfaces.
It’s a trick. It’s chroot on steroids. You aren’t “inside” anything; you’re just being gaslit by the kernel.
# Let's look at the reality of your 'isolated' world
# Host kernel version - I'm running this on a dusty Ubuntu box
uname -a
Linux hardware-arch-01 5.15.0-76-generic #83-Ubuntu SMP Thu Jun 15 19:16:32 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
# Now look at a container's 'isolation'
docker inspect 7a2b3c4d5e6f
[
{
"Id": "7a2b3c4d5e6f...",
"State": {
"Status": "running",
"Pid": 45029
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/...",
"MergedDir": "/var/lib/docker/overlay2/...",
"UpperDir": "/var/lib/docker/overlay2/..."
},
"Name": "overlay2"
},
"Config": {
"Hostname": "app-container",
"Image": "node:18-alpine"
}
}
]
See that Pid: 45029? That’s the truth. Your “isolated” container is just a line item in the host’s process table. If that process decides to eat 100% of the CPU because you wrote a recursive loop in JavaScript, the host feels it. That’s where cgroups v2 comes in—the only thing keeping your sloppy code from crashing my entire rack. It limits memory, CPU, and I/O. But don’t think for a second it’s a hard barrier. It’s a suggestion enforced by a very tired kernel.
3. The Overlay2 Autopsy: A File System Made of Lies
Now, let’s talk about how Docker handles files. You think you have a “disk” in your container? Wrong. You have a stack of folders pretending to be a disk.
Docker uses a Union File System, specifically overlay2 these days. It’s a “copy-on-write” mechanism. When you build an image, every line in your Dockerfile creates a new layer. These layers are read-only tarballs sitting in /var/lib/docker/overlay2.
When you start the container, Docker throws a thin “read-write” layer on top. If you try to modify a file that exists in a lower layer, the kernel has to copy that file up to the top layer before you can write to it. This is why your database performance is garbage if you don’t use volumes. You’re making the kernel do a frantic dance of copying files every time you write a log line.
# This is where your 'images' actually live. It's just a mess of hex strings.
ls -l /var/lib/docker/overlay2
total 0
drwx------ 4 root root 72 Oct 12 10:45 0123456789abcdef...
drwx------ 4 root root 72 Oct 12 10:45 123456789abcdef0...
drwx------ 4 root root 72 Oct 12 10:46 23456789abcdef01...
drwx------ 3 root root 48 Oct 12 10:46 l
The l directory there? Those are shortened symbolic links because the kernel has a limit on how long a mount argument can be. We’ve reached a point in software engineering where we have to use aliases for our aliases because the abstraction is too long for the OS to read. Let that sink in.
Every time you do an apt-get update in a Dockerfile, you’re adding 300MB of metadata to a permanent layer that will haunt your disk space forever. And you wonder why the SSDs in the SAN are crying.
4. The Networking Nightmare: Bridges, Veth, and NAT
If the file system is a lie, the networking is a hallucination.
When Docker starts, it creates a virtual bridge called docker0. Every container gets a veth (virtual ethernet) pair. One end of the pipe is inside the container’s network namespace (the CLONE_NEWNET thing I mentioned), and the other end is plugged into the docker0 bridge on the host.
To get traffic from the outside world into your container, Docker uses iptables. It’s a mess of Network Address Translation (NAT). Every packet entering your “high-performance” Go microservice has to be mangled by the host’s kernel, rewritten, and shoved through a virtual pipe.
Back in my day, a packet went from the wire to the NIC to the application. Now, it goes:
1. Physical NIC
2. Host Kernel (iptables PREROUTING)
3. Bridge Interface (docker0)
4. Virtual Ethernet Pair (veth)
5. Container Network Namespace
6. Application
And you people have the audacity to ask why the latency is spiking. You’ve built a Rube Goldberg machine for bits and called it “modern infrastructure.”
5. The Security Delusion: Why “Isolated” Doesn’t Mean “Secure”
This is the part that keeps me up at night. People treat Docker containers like they’re impenetrable vaults. “Oh, it’s in a container, it’s fine.”
No, it’s not fine.
Because you’re sharing the same kernel, a vulnerability in a syscall—like copy_file_range or some obscure io_uring bug—can allow a process to break out of the container and get root on the host. In a VM, you have to break the guest kernel, then the hypervisor. In Docker, you just have to trick the host kernel into thinking you’re allowed to talk to it.
Most of you run your applications as root inside the container. You think, “It’s okay, it’s a container root.” But if I manage to exploit your shitty Node.js app and you haven’t configured your seccomp profiles or AppArmor correctly, I’m one kernel exploit away from owning your entire cluster.
Docker tries to help with “Capabilities.” It drops things like CAP_SYS_ADMIN by default. But then some developer gets a “Permission Denied” error, looks up a fix on StackOverflow, and adds --privileged to their run command. Congratulations, you just gave the container permission to do almost anything to the host hardware. You might as well just give me your SSH keys and go home.
6. The Verdict: When to Stop Being Lazy
So, what is docker?
It’s a tool for managing the complexity of your own making. It’s a way to package an environment so that the “works on my machine” problem becomes “it works in this specific, resource-constrained, kernel-namespaced environment.”
Is it useful? Yes. It’s useful for CI/CD pipelines where I need to spin up a clean build environment and tear it down ten seconds later. It’s useful for ensuring that the version of Python you’re using in dev is the same one I’m running in production.
But it is not a replacement for understanding how an operating system works. It is not a magic wand that makes your code “scalable.” If your app is a bloated, synchronous, memory-leaking pile of garbage, putting it in a container just makes it a portable, isolated pile of garbage.
Stop using it because it’s trendy. Stop using it to hide the fact that you don’t know how to write a proper Makefile or manage a library path. Use it because you need reproducible environments. Use it because you need to limit the blast radius of your dependencies.
But for the love of all that is holy, stop acting like it’s magic. It’s just a bunch of tar files and kernel flags. Now, if you’ll excuse me, I have to go find out why a “simple” containerized update just triggered an OOM kill on a database node that has 256GB of RAM.
I miss the days when we just had to worry about the hardware catching fire. At least the fire was honest.
Technical Appendix for the Uninitiated
If you’ve read this far and still think I’m just being a “hater,” let’s look at the actual syscalls. When you run a container, the Docker daemon (or containerd these days, because we needed more layers of abstraction) isn’t doing anything you couldn’t do yourself with a bit of C code and some patience.
The Syscall Chain:
1. unshare(): This is the heart of it. It allows a process to disassociate parts of its execution context that are currently being shared with other processes.
2. mount(): Docker uses this to set up the rootfs. It mounts the merged layer of the overlay2 filesystem as the new root.
3. pivot_root(): This is the more secure version of chroot. It moves the root file system of the current process to a new directory and puts the old root in another directory.
4. sethostname(): Because why not let the container think its name is f3a1b2c3d4e5?
The Memory Allocation Reality:
In a VM, the memory is “pinned.” If you give a VM 8GB, that 8GB is gone from the host’s perspective. In Docker, because of the shared kernel, the memory management is handled by the host’s MMU (Memory Management Unit). If your container isn’t using its allocated 8GB, the host can give it to someone else. This sounds “efficient” until you realize it leads to “noisy neighbor” syndrome. One container starts thrashing the swap, and suddenly every other container on that physical CPU socket is waiting on I/O.
The Docker Engine Versioning:
We are currently dealing with Docker Engine v24.0.7. Over the years, they’ve moved from a monolithic daemon to a decoupled architecture: docker (the CLI) -> dockerd (the daemon) -> containerd (the supervisor) -> runC (the actual executor).
Every one of those arrows is a socket connection or a process fork. Every one of those is a potential point of failure. We’ve replaced a single binary with a four-stage relay race just to start a process.
Final Advice:
If you want to actually understand “what is docker,” stop using the docker command for a day. Try to build a container manually using ip netns, chroot, and cgroups. Once you see how the sausage is made, you’ll realize why I’m so grumpy. It’s not that the technology is bad—it’s that it’s being used as a crutch for a lack of fundamental systems knowledge.
Now get out of my office and go check your image layers. You’ve got curl and vim installed in a production image, and it’s a security nightmare waiting to happen.
Related Articles
Explore more insights and best practices: