TLDR:
- "I'm running Ubuntu in Docker" is a lie. What you actually have is a process with Ubuntu OS libraries and an environment on top of the host kernel.
- Your container's memory limit isn't just your app's memory. Heap, thread stacks, JIT cache, page cache, the garbage collector's own bookkeeping all draw from the same budget, and the kernel is the one holding the OOM trigger.
- The gap between "a container" and "a real OS" is invisible right up until a CPU throttle, an OOM kill, or a mounted Docker socket turns it into an incident.
Ask a room full of developers what's running in their container and most of them will say something like "Ubuntu" or "Alpine" or "Debian." It's a convenient answer, but it's wrong. What you actually have is a process with Ubuntu OS libraries and an environment, running on top of the host kernel. In a perfect world you'd never need to know the difference. The image behaves the same wherever it runs, the app responds, nobody gets paged. But we don't live in that world, and the day something breaks in a way that makes no sense, the difference between "a container" and "a Linux OS" is where the answer might be hiding.
So let's be precise about what you actually have.
What a real Linux OS does
A full Linux OS owns the whole lifecycle of the machine. It starts from a bootloader, brings up the kernel, and hands off to an init system (systemd, on most distros) that starts everything else.
From there, it runs everything. It manages hardware and devices, users and permissions, networking, logging, background services, packages, storage, and the security controls around all of it. It also decides who gets what: CPU time, memory, disk, processes. Everything on the machine answers it.
Think of it as a house. Applications, system services, the init system, the filesystem, the kernel, the hardware underneath, all of it lives in the same structure and the house owns the plumbing.
What you actually get in a container
A container isn't a house at all. Think of it as a rented room: it has its own furniture, its own view out the window, its own name on the door, but the building belongs to the host, and so does the foundation. That foundation is the host Linux kernel.
In more technical terms, a container is an isolated process environment: it gets a packaged filesystem, its own view of processes and networking and mounts, plus environment variables and resource limits.
What it doesn't get is a kernel of its own. It doesn't boot, it doesn't touch hardware, and unless you're running something like an LXC system container, it usually doesn't bother with a full init system like systemd either. The process inside sees its own little world. The host kernel is still running the whole building.
Container A Container B Container C
Filesystem Filesystem Filesystem
Processes Processes Processes
Namespaces Namespaces Namespaces
-----------------------------------------
Shared host Linux kernel
Shared host hardware or VM
That shared foundation is the whole trick. It's also where the danger lives, but that's a problem for later in this post.
Who draws the walls: namespaces and cgroups
If every container shares one kernel, what stops them from wandering into each other's rooms? Two kernel features do the work: namespaces and cgroups. Namespaces handle visibility, deciding what a process is even allowed to see. Cgroups are the meter: they don't care what the process sees, only how much of the host it's allowed to use up.
Namespaces: the view out the window
A namespace gives a process an isolated view of kernel resources that are still, underneath, managed by the same host kernel. The process thinks it has the place to itself. It doesn't, but the illusion holds.
- A PID namespace gives the container its own process tree. Inside, your app might be PID 1. On the host it's some unremarkable PID in the thousands.
- A network namespace comes with its own interfaces, routing table, firewall rules, and ports. That's why localhost inside the container means the container, not the host.
- A mount namespace hands over a private filesystem layout: image layers, mounted volumes, bind mounts, its own /proc. The host's filesystem stays out of view unless you explicitly let it in.
- A user namespace maps users inside the container to users outside it, so a process can look like root inside while mapping to an unprivileged user on the host.
- The UTS namespace isolates the hostname, and the IPC namespace isolates inter-process communication objects like shared memory and message queues.
Cgroups: the meter on the wall
Cgroups (control groups) are where the metering happens. The container runtime drops the container's processes into a cgroup, and from there the kernel tracks and caps what they use: CPU time (how much the container gets, or how hard it competes with everything else), memory (the ceiling before the kernel starts reclaiming or killing), the number of processes and threads it can spawn, disk I/O bandwidth, and which device files it can open. Anything the kernel can account for, a cgroup can limit.
Put namespaces and cgroups side by side against a real OS and the difference gets concrete:
|
Feature |
Standard Linux OS |
Linux container |
|---|---|---|
|
Kernel |
Its own |
The host's |
|
Boot process |
Full boot |
A process just starts |
|
Init system |
Usually present |
Usually absent or minimal |
|
Filesystem |
Real system root |
Image layers + mounts |
|
Processes |
Whole-machine view |
Namespaced view |
|
Networking |
Real host/VM stack |
Virtualized namespace, bridge, overlay, NAT |
|
Resource control |
Whole-OS scheduler |
cgroup limits |
|
Security boundary |
OS/VM boundary |
Kernel isolation, weaker than a VM |
Image, container, OS: three things mashed together
There's one more knot to untie, because "container" and "container image" get used as if they're the same thing, and neither is the OS.
A container image is a packaged filesystem plus metadata: application files, libraries, binaries, config, certificates, package metadata, sometimes a shell. An Ubuntu-based image, for example, carries an Ubuntu-style filesystem with /bin, /lib, /etc, a package database, and the usual user-space tools. But an image isn't running anything. It has no processes, no CPU or memory use, no IP address. It's a template sitting on a disk.
A container is what you get when the runtime starts a process from that image. It takes the image, adds a writable layer on top, sets up namespaces, applies cgroup limits, wires up mounts and networking, and starts the main process. That process becomes PID 1 inside the container's PID namespace, and now you have something alive.
So the honest version of "I'm running Ubuntu in Docker" is a bit of a mouthful: I'm running a process from an Ubuntu-based image, using the host Linux kernel, inside an isolated container environment. It's the version to keep in your head, because the short one is exactly what leads people astray when things break.
So why should you care?
Fair question. If the container starts, the app answers, and nobody pages you at 3 a.m., it's tempting to file all of this under trivia.
Here's the catch: the difference between a container and an OS is precisely where a lot of production problems live. They hide at the boundary between the container, the runtime, and the host. And they surface in two places first: resource limits and security.
Where it hurts first: resource limits
A container's resource limits apply to the entire process tree, not just to your application. Your app, its worker processes, its threads, native libraries, caches, sidecars, and all the runtime overhead share one budget for CPU, memory, I/O, and process count.
Memory is where this bites hardest, because the memory limit is a hard boundary the kernel enforces through cgroups. Hit it, and the kernel first tries to reclaim memory. If it can't recover enough, the container goes into an out-of-memory state and the kernel kills one of its processes.
The trap is assuming your application data is the only thing using that memory. Every runtime carries overhead you don't see in your own code:
- Java spreads memory across heap, metaspace, thread stacks, direct buffers, the JIT code cache, native libraries, and the garbage collector's own bookkeeping.
- With Python you're also paying for interpreter memory, object allocations, extension modules, thread stacks, and allocator fragmentation.
- Go carries its heap, goroutine stacks, runtime metadata, GC structures, and whatever cgo allocates natively.
- Databases and proxies lean on caches, connection buffers, background workers, the page cache, and sometimes memory-mapped files on top of all that.
So the real equation is:
container memory limit = application memory + runtime memory + native memory + thread stacks + filesystem cache + overhead
CPU limits play a different trick. A CPU limit doesn't necessarily mean "this container gets one core." More often it means the container runs freely for a bit, then gets throttled once it's used its allotted CPU time. The result is slower requests, longer GC pauses, and latency spikes that show up in your dashboards with no obvious cause. Worse, some runtimes don't even notice the limit: they read the host's CPU count and size their thread pools or parallel GC for a machine they don't actually have. Verify what your runtime detects. Don't trust the defaults.
Then there are PID limits. A container can be capped on how many processes or threads it can create, which matters for JVM thread pools, Node.js workers, Python multiprocessing, and web-server worker models. A PID limit protects the host from a fork bomb, but it will also break an app that assumed it could spawn threads forever.
If you want to catch these before they catch you, watch the right numbers:
- memory usage versus the limit, not just usage
- OOM-kill events and container restart reasons
- CPU throttling, not only CPU usage
- process and thread count against the PID limit
- disk and network I/O where it's relevant
- application latency and error rate alongside the container metrics
docker stats gives you a live view of CPU, memory, network, and block I/O for a quick look. In production, lean on your orchestrator and monitoring stack to line those metrics up against actual application behavior.
A good habit: set explicit CPU, memory, and PID limits, then load-test with those limits switched on. That's how you find where the walls are while it's still a test and not a 3 a.m. page.
Where it hurts next: security
Your container shares the host kernel with every other container and with the host's own processes. That's what makes containers cheap and fast. It's also a shared risk: one kernel bug, one careless privilege, one wrong mount, and a compromised container can reach past its own walls. It can read things it shouldn't, modify host files, take over the container runtime, poke at other workloads, or in the worst case escape isolation entirely.
The usual suspects:
- A privileged container hands over broad access to host devices and a long list of kernel features.
- Linux capabilities split root into smaller pieces, but the powerful ones (SYS_ADMIN, NET_ADMIN, SYS_PTRACE) still give a container serious reach.
- Host mounts like /etc, /proc, /sys, /var/lib, or /var/run/docker.sock can expose the host to whatever's inside.
- Running as root inside the container is a real risk, especially without user namespaces or rootless mode.
- Bloated images carry outdated packages, vulnerable libraries, unused tools, stray shells, and the occasional leaked secret.
The mindset that fixes most of this is boring and effective: reduce what the container can do.
- Run the app as a non-root user.
- Never run a privileged container in production!
- Drop the capabilities you don't need.
- Use seccomp to block dangerous system calls.
- Use minimal, regularly scanned, up-to-date images so there's less inside to exploit. This is the whole idea behind hardened base images like BellSoft Hardened Images, which run non-root and strip out package managers and other non-essentials on top of the small Alpaquita Linux base.
- Use AppArmor or SELinux to fence in what the process can touch.
- Avoid mounting sensitive host paths, especially the Docker socket!
Containers shrink the blast radius, but they never zero it out. Treat every container as an application process that has some isolation and is absolutely not impenetrable, and you'll make far better decisions about what to lock down.
The short version
A Linux container is not a tiny Linux OS. It's an isolated process on a shared kernel, fenced in by namespaces and capped by cgroups. Get that one idea straight and a whole category of confusing incidents starts to make sense, because performance and security problems tend to happen exactly at the seam between the container, the runtime, and the host.
Two things to do with this today: set explicit CPU, memory, and PID limits and load-test with them on, and start from a minimal, non-root, regularly scanned image instead of a general-purpose one. Everything else is easier once those two are in place.
If you want to go deeper, especially on the JVM side, these two are worth your time:






