← Back

2026

What are containers?

The mental models that finally made containers click: layers, volumes and networking.

I remember the first time someone told me to “just throw it in a container.” I nodded like I knew what that meant. I did not.

After spending a good chunk of time in test automation, I kept bumping into Docker. Every CI pipeline had it. Every staging environment ran on it. And every time something broke, someone would say “works on my machine” which is, ironically, exactly the problem containers were invented to solve.

So let me break down what containers actually are, how they work under the hood and why they've become the default way to ship software.

The “Works On My Machine” Problem

Before containers, deploying software was a mess of environment differences. Your app might run fine on your laptop with Python 3.9, some specific library versions, a particular OS config, but blow up on the server running Python 3.7 with slightly different dependencies.

The old solution was virtual machines. You'd spin up a full OS image, install everything you needed and ship that. It worked, but VMs are heavy: gigabytes, minutes to boot and a separate OS kernel per machine.

Containers solve the same problem with a fundamentally different approach.

What a Container Actually Is

A container is not a virtual machine. It doesn't have its own kernel. It shares the host machine's kernel but runs in an isolated process space.

The analogy

Virtual Machine

Renting an entire apartment.

Full OS, gigabytes, minutes to boot.

Container

Renting a room in a shared house.

Same kernel, isolated space, starts in seconds.

Under the hood, Linux containers rely on two kernel features:

Namespaces

Isolate what a process can see. A container has its own view of the filesystem, network interfaces, process IDs and more. It genuinely cannot see what's happening in other containers or on the host (unless you explicitly allow it).

cgroups(control groups)

Limit what a process can use. cgroups let the kernel enforce resource limits on CPU, memory and disk I/O so one container can't starve out everything else on the machine.

That's really it. Containers are just Linux processes running in isolated namespaces with resource limits applied. Docker and other container runtimes are tooling built on top of those kernel primitives to make working with them actually manageable.

Images vs Containers

Image

The blueprint.

Built in layers. Each Dockerfile instruction adds a layer that gets cached and reused. Only changed layers and everything after them get rebuilt.

Container

A running instance of the blueprint.

Docker adds a thin writable layer on top of all the read-only image layers. Changes disappear when the container stops, unless you use a volume.

A Real Example

Here's a stripped-down Dockerfile for a Node.js app, with each instruction annotated:

FROM node:20-alpine

Start from an existing image. Alpine is a minimal Linux distro that keeps the final image small.

WORKDIR /app

Set the working directory inside the container.

COPY package*.json ./

Copy dependency manifests first before the rest of the code.

RUN npm ci

Install dependencies. This layer gets cached. If your source changes but dependencies don't, Docker reuses this, which means faster builds.

COPY . .

Copy the rest of the source code.

EXPOSE 3000

Document the port the app listens on.

CMD ["node", "server.js"]

What runs when the container starts. Only one CMD per Dockerfile; the last one wins.

Build it with docker build -t my-app ., run it with docker run -p 3000:3000 my-appand your app is running in a container on your machine exactly as it would in production.

Networking Between Containers

When you run multiple containers (e.g. a web server and a database) they need to talk to each other. By default, each container gets its own network namespace so they're isolated.

Docker handles this with virtual networks. Containers on the same Docker network can reach each other by container name. Your app container can connect to postgres:5432 if the database container is named postgres and they're on the same network. No IP addresses to hardcode, no DNS configuration headaches.

Docker Compose makes this even simpler: define all your services in a docker-compose.yml and Compose handles creating the network and wiring everything together. One docker compose up and your entire stack is running.

Why This Actually Matters

When your app and all its dependencies are packaged into an image, the environment stops being a variable. The same image that passed tests in CI is the exact same image that gets deployed to production. Not “basically the same”, identical.

That consistency is what makes modern deployment pipelines possible. It's what lets Kubernetes schedule your workloads across dozens of machines without you thinking about which libraries are installed where. It's why “works on my machine” is no longer an acceptable answer.

Where to Go From Here

Containers are the foundation, but the ecosystem goes much deeper:

Container registries

Docker Hub, AWS ECR. Store and distribute your images.

Kubernetes

Orchestrate containers at scale: scheduling, scaling, self-healing.

Container security

Scan images for vulnerabilities, run as non-root, limit capabilities.

Multi-stage builds

Keep production images small by separating build and runtime environments.

Once you actually understand what a container is, a process in a namespace with resource limits, the rest starts to make a lot more sense. The tooling is just managing that core primitive at different levels of abstraction.

Final Thoughts

Containers are one of those things that seem complicated from the outside and click into place once you understand the two or three concepts underneath them.

Namespacesgive isolation
cgroupsgive resource control
Layersgive reproducibility

The next time someone tells you to “just throw it in a container,” you'll know exactly what they mean and you might even have opinions about how the Dockerfile should be structured.