Understanding Docker: Containers, Images, Layers and the System Behind Them

The first time I used Docker, I thought it was just a packaging tool.

Write a Dockerfile, run docker build, then docker run.

Simple.

Until one day my database container restarted and all the data was gone.

That moment made me realize something important:

Docker isn’t just a CLI tool.

It’s a system of concepts working together.

Most confusion around Docker comes from not seeing how these pieces connect:

  • images
  • containers
  • layers
  • volumes
  • networks
  • orchestration

Once the mental model clicks, Docker suddenly becomes much easier to reason about.

The Docker Ecosystem

Before diving into the details, it helps to see the big picture.

Docker Engine manages several core building blocks.

  • Images: blueprints used to create containers.
  • Containers: running instances of images.
  • Volumes: persistent storage outside the container lifecycle.
  • Networks: service discovery and communication.
  • Docker Compose: defines multi-container applications.

Understanding how these pieces relate to each other is the key to understanding Docker.

flowchart LR
    Engine["Docker Engine"]
    Img["Images"]
    Ctr["Containers"]
    Vol["Volumes"]
    Net["Networks"]
    Comp["Docker Compose"]

    Engine --> Img
    Engine --> Ctr
    Engine --> Vol
    Engine --> Net
    Comp --> Img
    Comp --> Ctr
    Comp --> Vol
    Comp --> Net

Docker Is Not a Virtual Machine

One of the most common misconceptions is thinking Docker containers are similar to virtual machines.

They are not.

Virtual machines virtualize hardware. Containers virtualize the operating system environment.

Containers share the host kernel and isolate processes using Linux primitives such as:

  • namespaces
  • cgroups
  • union filesystems

This is why containers start quickly and consume fewer resources than VMs.

flowchart TB
    subgraph VM["Virtual Machine"]
        HW["Hardware"]
        Hyper["Hypervisor"]
        G1["Guest OS"]
        App1["App"]
        HW --> Hyper --> G1 --> App1
    end

    subgraph Container["Container Model"]
        Host["Host OS / Kernel"]
        Runtime["Container Runtime"]
        C1["Container A"]
        C2["Container B"]
        Host --> Runtime
        Runtime --> C1
        Runtime --> C2
    end

Images vs Containers

One of the most important Docker concepts is the difference between images and containers.

Think of it like this:

  • Image = blueprint
  • Container = running instance

Images are:

  • immutable
  • read-only
  • reusable

Containers are:

  • runtime instances
  • writable
  • ephemeral

Running a container is essentially instantiating an image.

docker run nginx

Docker will:

  1. pull the image
  2. create a container
  3. start the process inside it
flowchart LR
    Image["Image (immutable blueprint)"]
    Create["docker run"]
    Container["Container (runtime instance)"]
    Process["Running process"]

    Image --> Create --> Container --> Process

Layers: Why Docker Builds Are Fast

Docker images are built using a layered filesystem.

Each Dockerfile instruction creates a new layer.

Here is a simple Go example:

FROM golang:1.26

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o app

CMD ["./app"]

Layer breakdown:

  • Layer 1 -> Base image (golang)
  • Layer 2 -> Dependencies
  • Layer 3 -> Source code
  • Layer 4 -> Compiled binary

If only the application code changes, Docker can reuse:

  • the base image
  • the dependency layer

and rebuild only the final layers.

flowchart TB
    L1["Layer 1: golang:1.26"]
    L2["Layer 2: go mod download"]
    L3["Layer 3: application source"]
    L4["Layer 4: compiled binary"]

    L1 --> L2 --> L3 --> L4

Docker Layer Cache: Why Instruction Order Matters

Docker caches each layer during the build process.

Because dependencies are copied first, Docker only re-downloads them when go.mod changes.

This makes builds significantly faster.

When cache behavior is wrong, builds slow down immediately.

COPY . .
RUN go mod download
RUN go build

In this version, any source code change invalidates the dependency layer.

Now every build must:

  • download dependencies again
  • rebuild everything

This is why Dockerfile optimization usually follows a simple rule:

Dependencies first, source code later.

flowchart LR
    Good1["COPY go.mod go.sum"] --> Good2["RUN go mod download"] --> Good3["COPY . ."] --> Good4["RUN go build"]

Multi-Stage Builds

In production environments you usually want the smallest possible image.

Go makes this easy using multi-stage builds.

FROM golang:1.26 AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o app

FROM alpine:3.20

WORKDIR /app

COPY --from=builder /app/app .

CMD ["./app"]

This approach:

  • builds the binary inside the Go image
  • copies only the compiled binary into the final image
  • removes build tools from production

The result is a dramatically smaller image.

Volumes: Solving the Persistence Problem

Containers are ephemeral.

If a container disappears, its filesystem disappears too.

Docker solves this with volumes.

docker run -v postgres-data:/var/lib/postgresql/data postgres

Now database data survives container restarts.

This is the line many people only understand after they lose data once.

Containers are disposable. State should not live inside them unless you explicitly accept that risk.

flowchart LR
    C["Postgres Container"] --> M["Mount point: /var/lib/postgresql/data"]
    M --> V["Docker Volume: postgres-data"]

Docker Networking

Docker containers communicate through virtual networks.

The most common network driver is bridge.

Docker provides two key features here.

Internal DNS

Containers can communicate using service names.

For example:

  • postgres:5432
  • redis:6379

Docker resolves these names automatically when the services are on the same network.

Network isolation

Each Docker network runs in its own namespace.

This provides:

  • service isolation
  • simple networking
  • automatic service discovery
flowchart LR
    A["Host Machine"]

    subgraph N["Docker Network"]
        B["API Container"]
        C["Worker Container"]
        D["Postgres Container"]
        E["Redis Container"]
    end

    A --> B
    A --> C
    A --> D
    A --> E

    B -->|"postgres:5432"| D
    B -->|"redis:6379"| E
    C -->|"postgres:5432"| D

Docker CLI vs Docker Compose

You can run containers manually using the Docker CLI.

docker network create app-network

docker run -d \
  --name postgres \
  --network app-network \
  -v db-data:/var/lib/postgresql/data \
  postgres

docker run -d \
  --name api \
  --network app-network \
  -p 8080:8080 \
  my-api

This works, but managing infrastructure this way becomes messy quickly.

You must manually handle:

  • networks
  • volumes
  • container lifecycle
  • service dependencies

The system exists, but the definition of the system is scattered across shell commands.

The Same Setup with Docker Compose

Docker Compose allows defining the entire system in a single file.

version: "3"

services:
  api:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db

  db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Now the whole system runs with:

docker compose up

Compose automatically manages:

  • networking
  • service discovery
  • volumes
  • container lifecycle
  • startup order

That is where Docker starts to feel less like a command-line utility and more like an application runtime model.

flowchart TB
    Compose["docker-compose.yml"]
    Up["docker compose up"]
    S1["api"]
    S2["db"]
    V["db-data volume"]
    N["default network"]

    Compose --> Up
    Up --> S1
    Up --> S2
    Up --> V
    Up --> N

The Docker Mental Model

Once these pieces connect, Docker becomes much easier to understand.

Images      -> blueprints
Containers  -> running instances
Layers      -> build caching
Volumes     -> persistent data
Networks    -> service discovery
Compose     -> orchestration

Docker is not just:

  • docker build
  • docker run

It’s an ecosystem of concepts working together.

And once that mental model clicks, Docker stops feeling like magic and starts feeling predictable.

Further Reading