containers

Docker Is Weird: A Container Is a Process in Disguise

Docker Is Weird: A Container Is a Process in Disguise

Docker Is Weird: A Container Is a Process in Disguise

The first time you run this command, it feels like you have summoned a tiny Ubuntu computer:

docker run --rm -it ubuntu bash

A shell appears. The filesystem looks familiar. You can install packages, create files, and pretend there is a small server hiding inside your terminal.

But there is no miniature Ubuntu kernel waiting underneath. A Docker container is an ordinary Linux process wearing several layers of isolation. That sounds less magical, although the engineering behind it is anything but simple. Once you look beneath the command, Docker becomes wonderfully strange.

A container is not a tiny virtual machine

A virtual machine includes a complete operating system kernel, along with virtual hardware and the programs that run on top of it. Containers take a different route: several isolated processes share one Linux kernel.

The word isolated does a lot of work here. Linux provides several separate features that container tools combine:

  • Namespaces change what a process can see. A process namespace can give a container its own process list, while a network namespace gives it separate interfaces, routes, and ports.
  • Control groups, usually called cgroups, track and limit resources such as memory, CPU time, and block-device activity.
  • Capabilities divide the traditional powers of the root user into smaller permissions. A container may have user ID 0 inside its filesystem without receiving every dangerous privilege on the host.
  • seccomp, short for secure computing mode, filters system calls. A system call is a request from a program to the kernel, such as opening a file or creating a new process.
  • Mount isolation gives the process its own view of mounted filesystems.

There is no single Linux feature named “container.” A container runtime assembles these pieces, configures them in the right order, and starts the application inside the resulting environment. The container is real, but it is not a separate machine.

That is why a process can be visible from outside the container. On a Linux host, you can run:

docker run --rm -d --name sleepy alpine sleep 600
docker inspect -f '{{.State.Pid}}' sleepy

The second command prints the process ID assigned by the host. Inside the container, that same process may appear as process 1 because the container has its own process ID namespace. One process can have different IDs in different namespace views. The /proc filesystem reflects those views, which makes container debugging feel like looking at the same person through two security cameras pointed from different rooms.

Docker images are filesystem ingredients

A Docker image is not an operating system in the virtual-machine sense. It is a packaged filesystem plus metadata describing how to start a process.

Images are usually built from layers. Each layer records filesystem changes, such as adding a package or copying application code. Registries distribute those layers, often as compressed archives. Docker then unpacks and combines them using a storage backend.

On classic Linux installations, that backend might be overlay2, which presents several read-only layers beneath a writable container layer. Fresh Docker Engine 29.0 and later installations use the containerd image store by default, where snapshotters assemble the filesystem layers. The terminology changes, but the important idea remains: the process sees a merged root filesystem rather than a complete operating system with its own kernel.

This writable layer explains a common Docker surprise. When a running container changes a file, the change belongs to that container's top layer. Remove the container with --rm, and that layer disappears. The original image remains untouched.

You can see the shared-kernel nature with a small experiment:

docker run --rm ubuntu cat /etc/os-release
docker run --rm ubuntu uname -r

The first command reports the user-space distribution stored in the image. The second reports the kernel provided by the Docker host, or by the Linux virtual machine hidden behind Docker Desktop. An Ubuntu image does not bring an Ubuntu kernel along for the ride.

The root filesystem setup may involve chroot, pivot_root, bind mounts, and layered filesystems. Saying “Docker points the process at a folder” is a useful first sketch, but the real implementation is a carefully prepared mount namespace with security and lifecycle rules around it.

The whale is a stack of smaller programs

The docker command is not the container runtime. It is a client that sends requests to the Docker daemon, a long-running background service commonly called dockerd.

A simplified path looks like this:

Docker CLI → dockerd → containerd → runc → Linux kernel

The daemon manages Docker objects and exposes an API. containerd handles much of the image and container lifecycle work. runc is a low-level runtime that reads an Open Container Initiative, or OCI, configuration and turns it into a running process with namespaces, cgroups, mounts, capabilities, and other settings.

So what does docker run actually do? It may pull image layers, prepare a root filesystem, create a network endpoint, make a cgroup, construct an OCI configuration, ask runc to create the process, and then connect your terminal to its standard input and output. The command feels like one action because Docker hides the handoffs.

That architecture is still shifting. Docker Engine 29.7.0 introduced an experimental mode that can run containerd inside the Docker daemon process rather than supervising it as a separate process. The layers are being rearranged, not removed.

Go gets awkward at the process boundary

Calling Go a bad language for container runtimes is too harsh, but the frustration has a real source. Go programs use goroutines, a garbage collector, and a runtime that manages operating-system threads. Those features are excellent for servers. They become delicate during the few milliseconds when a runtime is creating namespaces and preparing a child process.

The classic Unix fork operation duplicates a process, but after a multithreaded program forks, the child contains only the thread that called fork. Locks held by other vanished threads remain locked. Runtime bookkeeping, signal handlers, and memory management can become dangerous too.

Go supports fork-and-exec operations internally, but it does not present arbitrary raw fork as a normal everyday building block. runc works around the boundary with a re-execution design. It starts another copy of its own executable through /proc/self/exe, passes a special argument such as init, and uses a small C component before the Go runtime has fully initialized. That early C code performs operations that need a carefully controlled process state; Go then takes over for the rest.

It is an unusual arrangement. It is also a good reminder that “launch a process” becomes a serious systems problem when the process must be born inside several namespaces, a cgroup, a new root filesystem, and a restricted security profile.

Rootless Docker moves the weirdness into user space

On Linux, the Docker daemon traditionally runs as root because many kernel operations involved in mounts, networking, device access, and namespace setup require elevated privileges. Access to the Docker socket can therefore be nearly equivalent to having root access on the host.

Modern rootless Docker changes the model. User namespaces can make your regular host account appear as root inside a private namespace while remaining unprivileged outside it. The daemon and containers can run without a root-owned daemon, although compatibility and performance vary.

Networking is where the disguise becomes most elaborate. A privileged runtime can create virtual Ethernet devices, configure bridges, and manipulate kernel routing. Rootless mode cannot freely do all of that, so tools such as RootlessKit use user-space networking implementations including slirp4netns or pasta. Those approaches work, but they can have different port-forwarding behavior and lower throughput than a kernel-managed network path.

Docker Desktop hides another layer on macOS. Linux containers run inside a Linux virtual machine managed by Docker Desktop, so the Docker CLI on macOS is talking to a Linux Docker Engine rather than asking the macOS kernel to provide Linux container primitives.

The strange part is the lesson

Docker containers are not fake. They are real processes, real filesystems, real network namespaces, and real kernel resource controls arranged to look like small computers.

That is why writing a toy container runtime teaches so much. Each friendly Docker concept eventually leads to a system call, a mount flag, a namespace handle, a cgroup file, or a carefully timed exec. The whale logo is the polished surface. Underneath it is a pile of Linux mechanisms cooperating so convincingly that we forget how many moving parts are involved.

ahsan

ahsan

Hello! I am Mr Ahsan, the writer of the Website. I am from Netherland. I like to write about technology and the news around it.

Comments (0)

No comments yet. Be the first to respond!

Leave a Comment

Your comment will be visible after review.