← Back to Notes

How Docker Containers Work

A clear explanation of what Docker containers actually are, how they differ from virtual machines, and how images, layers, and Dev Containers fit together.

The Core Problem Docker Solves

Software depends on other software. A Python app might need Python 3.12, specific libraries, specific environment variables, and a specific OS configuration. If you install all of that directly on your machine, two things happen:

  • Your system gets cluttered with dependencies from every project you've ever worked on
  • Projects start conflicting with each other — Project A needs Python 3.9, Project B needs Python 3.12

Docker's answer: give every project its own isolated box with exactly what it needs, nothing more.


What a Container Actually Is

A container is not a virtual machine. This is the most common misconception. Here's the difference:

Virtual MachineDocker Container
Has its own OS kernelYesNo
Has its own filesystemYesYes
Has its own processesYesYes
Has its own networkYesYes
Boot timeMinutesMilliseconds
SizeGBsMBs
OverheadHighVery low

A container shares the Linux kernel with the host machine. What it gets its own copy of is everything above the kernel — the filesystem, the processes, the network interfaces, the environment variables.

This is made possible by two Linux kernel features:

  • Namespaces — the kernel creates isolated views of system resources. A container's processes think they're the only ones running. They can't see the host's processes, other containers' processes, or files outside their own filesystem.
  • cgroups (control groups) — the kernel limits how much CPU, RAM, and disk I/O a container is allowed to use.

Docker is essentially a tool that makes setting up and managing namespaces and cgroups easy.


Images vs Containers

This distinction trips up almost everyone at first.

An image is a blueprint — a read-only snapshot of a filesystem at a point in time. It contains OS files, a language runtime, libraries, and configuration. It just sits there. You can't run code in an image.

A container is a running instance of an image. When you start a container, Docker takes the image, layers a thin writable filesystem on top of it, sets up the namespaces and cgroups, and starts a process inside it. That running thing is the container.

The clearest analogy:

An image is like a class in programming. A container is like an instance of that class. You can spin up many containers from the same image, and they're all independent of each other.

What happens when you run a container

When you run docker run hello-world:

  1. Docker looks for an image called hello-world locally — if not found, moves to step 2
  2. Downloads it from Docker Hub (Docker's public image registry)
  3. Creates a container from that image
  4. Starts a process inside the container
  5. When the process exits, the container stops

Docker Hub

Docker Hub is a public registry of pre-built images — think of it as an app store for container blueprints. Microsoft, the Python Foundation, and thousands of others publish official images there.

When you pull a Python image, Node.js image, or any other base image, it comes from Docker Hub. The first pull takes time because it's downloading potentially hundreds of megabytes. Every subsequent use is instant because the image is cached locally.


How Layers Work

Images are built in layers, like a stack of transparent slides. Each layer contains only the difference from the layer below it.

A Python 3.12 image might look like this under the hood:

Layer 4: pip packages specific to your project
Layer 3: Python 3.12 runtime
Layer 2: Ubuntu base OS files
Layer 1: Bare Linux filesystem

This layered design is efficient — if you have ten different Python projects, they all share the same Ubuntu and Python layers on disk. Only the project-specific top layers differ.


What is a Dockerfile?

A Dockerfile is a plain text recipe that describes how to build a custom image. You start from an existing base image and add your own layers on top.

FROM python:3.12-slim               # start from the official Python image
WORKDIR /app                        # set the working directory inside the container
COPY requirements.txt .             # copy your dependencies list into the container
RUN pip install -r requirements.txt # install those dependencies
COPY . .                            # copy your actual code in
CMD ["python", "app.py"]            # the command to run when the container starts

Each line in a Dockerfile becomes a layer in the resulting image. When you build the image, Docker executes these instructions in order, caching each layer so rebuilds are fast.

T
TIP

Because each line is a cached layer, order matters in a Dockerfile. Put things that change rarely (base image, system dependencies) near the top and things that change often (your code) near the bottom. This keeps rebuilds fast — Docker only re-executes layers below the first changed one.


Where Your Files Live

This is a critical concept. Your code files are not stored inside the container — they live on your host filesystem. Docker mounts your project folder into the container, meaning the container can see and edit those files, but the files themselves live outside it.

Host filesystem                   Container
~/projects/my-app      <——>      /workspaces/my-app
    (real files)                  (mounted view)

This means:

  • Your code survives when a container is deleted
  • The container is just an environment — your files are always safe on the host
  • Changes you make inside the container are immediately reflected on the host, and vice versa
N
NOTE

Containers are designed to be disposable. Delete one, spin up another from the same image in seconds. Your work is always safe because it lives on the host filesystem, not inside the container.


What is a Dev Container?

A Dev Container is Docker with an extra layer of tooling designed specifically for development rather than production deployment.

When VS Code opens a Dev Container, it:

  1. Reads the .devcontainer/devcontainer.json file in your project folder
  2. Pulls or builds the Docker image specified in it
  3. Starts a container from that image
  4. Installs any VS Code extensions specified into the container
  5. Connects VS Code's backend (language server, debugger, terminal) to run inside the container

This means when you open a terminal in VS Code while inside a Dev Container, that terminal is running inside the container. The language runtime is inside the container. Nothing touches your host machine.

The devcontainer.json file

This file is the configuration for your Dev Container:

{
  "name": "Python 3",
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python"
      ]
    }
  }
}

It defines what image to use, what VS Code extensions to install inside the container, port forwarding rules, startup commands, and more.


Container Lifecycle

Containers have a simple lifecycle:

StateHow you get thereWhat it means
Runningdocker run <image> or docker start <container>Active process inside the container
Stoppeddocker stop <container> or process exitsContainer exists but no process is running
Removeddocker rm <container>Container is gone — image is unaffected
N
NOTE

Removing a container does not remove the image it came from. You can always create a new container from the same image instantly.


The Full Picture

Here's how all the layers stack together in a typical development setup:

Host OS (Windows, macOS, or Linux)
+-- Linux kernel (native or via WSL2/VM)
    +-- Docker Engine
        +-- Container
            +-- Language runtime (Python, Node, etc.)
            +-- Project dependencies
            +-- VS Code server (if using Dev Containers)
            +-- Mounted project files from host filesystem

Your editor runs on the host, but everything that executes — the language runtime, the terminal, the debugger — runs inside the container. When you're done, you close the container. Your system is exactly as clean as when you started.


Key Takeaways

  • Containers are not VMs — they share the host kernel and are far more lightweight
  • Images are blueprints, containers are running instances — like classes and objects in programming
  • Your files live outside the container — mounted in, always safe on the host
  • Containers are disposable — delete one, spin up another from the same image in seconds
  • Dev Containers wire VS Code into a container — your entire development environment is isolated and reproducible

Docker Commands Reference

# List running containers
docker ps

# List all containers including stopped ones
docker ps -a

# List downloaded images
docker images

# Run a container from an image
docker run <image-name>

# Run a container interactively with a terminal
docker run -it <image-name> bash

# Stop a running container
docker stop <container-name>

# Remove a stopped container
docker rm <container-name>

# Remove an image
docker rmi <image-name>

# Clean up all unused containers, images, and networks
docker system prune

# Pull an image from Docker Hub without running it
docker pull <image-name>

# View logs from a container
docker logs <container-name>
T
TIP

docker system prune is your best friend for reclaiming disk space. Docker images and stopped containers accumulate quickly and can eat gigabytes over time. Run it periodically to clean up anything unused.