# Containers Are Not Small VMs

A container is not a tiny virtual machine.

That sentence sounds obvious until you watch data disappear with `docker rm`, or two containers fail to find each other by name. The mistake is not cosmetic. It changes how you debug, where you put data, and how much isolation you assume you have.

I rebuilt my Docker mental model around one fact: **a container is a process on the host kernel with a restricted view of the machine.** Everything else - images, ports, volumes, the CLI - follows from that.

## The problem: "it looks like a VM"

When you start a VM, you get a booting operating system, a login prompt and a network interface. A container gives you a shell, a filesystem and an IP address too. The surface looks similar, so the brain files it under "small VM".

Then the assumptions break:

- The container starts in milliseconds and uses megabytes, not gigabytes.
- `uname -a` shows the host kernel.
- Nothing survives `docker rm` unless you mounted a volume.
- A kernel vulnerability can be relevant to every container on the host.

## Three ways to run a workload

| Model | What is virtualized | Isolation | Startup | Typical size |
| --- | --- | --- | --- | --- |
| Dedicated host | Nothing | Whole machine | Minutes | Whole machine |
| Virtual machine | Hardware | Separate guest kernel | Tens of seconds | GBs |
| Container | Processes | Shared host kernel | Milliseconds | MBs |

A VM runs a hypervisor, then a full guest OS, then your app. The hypervisor pretends to be hardware.

A container skips the guest OS. The app runs directly on the host kernel inside a restricted view built from namespaces, cgroups and a layered filesystem.

```text
VM:        app -> guest OS -> hypervisor -> hardware
Container: app -> runtime -> host kernel -> hardware
```

That missing guest OS is the whole trick. It is why a container image can be a few megabytes and why startup is measured in milliseconds.

## What a container actually is

Three Linux features do the work:

- **Namespaces** give the process its own view of PIDs, mounts, network, hostnames, IPC and user IDs.
- **cgroups** limit and account for CPU, memory and I/O.
- **A layered filesystem** (overlayfs and friends) assembles the container's root filesystem from image layers.

Docker did not invent these. It made them usable with a standard image format, a registry, a build system, a CLI and conventions for networks and volumes.

### What that means in practice

- **The kernel is shared.** A container escape or a kernel bug has a larger blast radius than a VM escape.
- **Root inside a container is close to root outside** unless you use rootless mode, user namespaces or a hardened runtime.
- **The writable layer is disposable.** It belongs to the container, not to your data.
- **The image carries userspace, not a kernel.** That is why a Linux image runs on Linux, and why Docker Desktop runs a Linux VM behind the scenes on macOS and Windows.

### When a VM is still the right call

Containers are not the answer to every isolation problem. A VM is still the better choice when:

- You need a different kernel or operating system. A Linux container cannot run a Windows kernel.
- You need hard multi-tenant isolation. A shared kernel is a larger blast radius.
- You need to simulate a full machine, including boot behaviour, kernel modules or nested virtualization.
- A vendor supports only a full OS image.

The honest framing: containers package applications; VMs provide machines. Most platforms use both, with containers inside VMs.

### Why the CLI feels inconsistent

Because images and containers are different objects. `docker images` lists templates. `docker ps` lists instances. `docker rmi` removes a template; `docker rm` removes an instance. Flags that look similar operate on different nouns.

Once you see two objects with different lifecycles, commands like `docker create` and `docker run` stop looking redundant.

### A 30-second test

When you are unsure whether something is an image or a container, ask: does it have runtime state? An image does not. A container does, even when it is stopped. That is why `docker ps -a` can show something that is not running.

### Where the VM boundary moves

On a Linux host, containers are processes on the host kernel. On macOS and Windows, Docker Desktop runs a Linux VM and the containers live inside it. That is why `docker info` can show a different kernel than your laptop, and why file-sharing performance differs between platforms. It is also why Docker Desktop needs a running VM before any container starts.

The same pattern appears in production. A Kubernetes node is often a VM, and the containers are scheduled inside it. The VM provides the machine boundary; containers provide the application boundary. Neither replaces the other.

Once you see that layering, "containers vs VMs" stops being a binary choice. The real question is which boundary you need at which level.

That is why the same image can run on a laptop, a CI runner and a cloud node without being rebuilt for each machine. The kernel determines what can run; the container determines what the app can see.

## The lifecycle that explains the CLI

An image is a read-only template. A container is an instance with a thin writable layer.

| State | How you get there |
| --- | --- |
| Image | `docker pull` or `docker build` |
| Created | `docker create` |
| Running | `docker start` or `docker run` |
| Stopped | `docker stop` |
| Removed | `docker rm` |

`docker run` is shorthand for pull (if needed), create and start. It is not "start the container I made earlier". That is `docker start`.

This is why a stopped container still exists, still appears in `docker ps -a`, and still takes disk space.

## Walkthrough: prove the model to yourself

Start with the client and daemon:

```bash
docker version
docker info
```

Run a throwaway container:

```bash
docker run --rm hello-world
```

Now prove the kernel is shared:

```bash
# Compare the host kernel with the container's view
uname -a
docker run --rm alpine uname -a
```

The version strings match. There is no guest kernel.

Run a real service in the background:

```bash
# -d detaches, --name gives it a stable name, -p maps host:container
docker run -d --name web -p 8080:80 nginx:1.27-alpine

docker ps
docker logs web
docker exec -it web sh
```

Inside the shell, look at PID 1:

```bash
cat /proc/1/cmdline; echo
```

You are looking at the container's entrypoint, not the host init process. Exit the shell and clean up:

```bash
docker stop web
docker rm web
docker rmi nginx:1.27-alpine
```

Notice that `stop` did not delete anything. `rm` did.

## The habits that follow

1. **Name containers you care about.** `--name` turns a random ID into something you can reason about.
2. **Use** `--rm` **for one-off commands.** It removes the container when it exits.
3. **Put data on a volume.** If it matters after `docker rm`, it does not belong in the writable layer.
4. **Publish ports deliberately.** `-p 8080:80` is host 8080 to container 80, and the host side binds broadly by default.
5. **Treat root in a container as root.** Use a non-root user in the image where you can.

## Gotchas

- **A stopped container is not gone.** `docker ps -a` shows it; `docker rm` removes it.
- `docker run` **creates a new container every time.** Use `docker start` for an existing one.
- **The writable layer disappears with the container.** Volumes and bind mounts are the exception.
- **The kernel is shared.** Containers are a packaging and isolation tool, not a hard security boundary.
- **Docker Desktop is a Linux VM.** On macOS and Windows, the "host" kernel you see belongs to that VM.
- **Architecture matters.** An `amd64` image will not run natively on an `arm64` host.
- `docker rmi -f` **forces image removal but does not remove containers.** It can leave them pointing at a missing image.

## Recap

- A container is an isolated process, not a small VM.
- VMs virtualize hardware; containers isolate processes on a shared kernel.
- Images are templates; containers add a disposable writable layer.
- `run` = pull + create + start; `start` is for an existing container.
- Data that must survive belongs on a volume.

If the mental model is right, the CLI stops being a list of flags and becomes a lifecycle you can reason about.

Next in this series: how Dockerfiles turn that lifecycle into reproducible images, and why instruction order decides whether your build takes ten seconds or ten minutes.

Try the five-command walkthrough above on a local machine. If it helps, share the post with someone who still thinks containers are small VMs.

## Source notes

- Repository notes: <https://github.com/rg0901/build-log/tree/main/DevOps-Tool/Docker>
- Docker overview: <https://docs.docker.com/get-started/docker-overview/>
- Linux namespaces: <https://man7.org/linux/man-pages/man7/namespaces.7.html>
