
Docker Core Architecture
- 9 installs
- 9 repo stars
- Updated July 8, 2026
- openaec-foundation/docker-claude-skill-package
Helps with devops & ci/cd tasks.
About
docker-core-architecture is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- docker-core-architecture
- DevOps & CI/CD
- AI-coding skill
Docker Core Architecture by the numbers
- 9 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,020 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/docker-claude-skill-package --skill docker-core-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/docker-claude-skill-package ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
docker-core-architecture
Quick Reference
Architecture Components
| Component | Role | Process |
|---|---|---|
| Docker CLI | User-facing command interface | docker |
| Docker Daemon | API server, image management, orchestration | dockerd |
| containerd | Container runtime supervision, image pull/push | containerd |
| runc | OCI-compliant container spawner | runc (exits after spawn) |
| BuildKit | Image build engine (default since Engine 23+) | buildkitd (within dockerd) |
Docker Object Types
| Object | Description | Key Command |
|---|---|---|
| Image | Immutable, layered filesystem template | docker build, docker pull |
| Container | Runnable instance of an image with writable layer | docker run, docker create |
| Network | Isolated communication channel between containers | docker network create |
| Volume | Persistent storage managed by Docker | docker volume create |
OCI Standards
| Standard | Purpose | Governs |
|---|---|---|
| OCI Image Spec | Portable image format | Layer format, manifest, config |
| OCI Runtime Spec | Container execution contract | Filesystem bundle, lifecycle, environment |
| OCI Distribution Spec | Image registry API | Pull, push, content discovery |
Critical Warnings
NEVER assume a container has persistent storage -- the writable container layer is deleted when the container is removed. ALWAYS use volumes or bind mounts for data that must survive container removal.
NEVER treat images as mutable -- images are immutable stacks of read-only layers. To change an image, ALWAYS build a new one. Using docker commit in production creates unreproducible, undocumented images.
NEVER send unnecessary files in the build context -- ALWAYS create a .dockerignore file. The entire build context directory is sent to the daemon before any build instruction executes.
NEVER confuse docker export/import with docker save/load -- export flattens all layers into a single filesystem tar (loses history and metadata). save preserves the full image structure with all layers, tags, and history.
NEVER run production workloads without resource limits -- containers without memory or CPU limits can consume all host resources. ALWAYS set -m and --cpus flags.
---
Architecture Diagram
Docker Architecture (Engine 24+)
+------------------+
| Docker CLI | User runs: docker build / run / pull / push
+--------+---------+
|
| REST API (Unix socket or TCP)
v
+--------+---------+
| Docker Daemon | dockerd
| | - Serves Docker API
| +------------+ | - Manages images, networks, volumes
| | BuildKit | | - Orchestrates container lifecycle
| +------------+ |
+--------+---------+
|
| gRPC API
v
+--------+---------+
| containerd | Container runtime supervisor
| | - Manages container lifecycle
| | - Pulls/pushes images (OCI compliant)
| | - Manages snapshots (layer storage)
+--------+---------+
|
| OCI Runtime Spec
v
+--------+---------+
| runc | OCI reference runtime
| | - Creates namespaces & cgroups
| | - Starts container process
| | - Exits after spawn (container runs independently)
+------------------+
|
v
+------------------+
| Container | Isolated process(es) with:
| | - Own PID, network, mount, UTS, IPC namespaces
| | - Resource limits via cgroups
| | - Union filesystem (read-only layers + writable layer)
+------------------+Request Flow
1. CLI sends REST API request to dockerd (via Unix socket /var/run/docker.sock) 2. dockerd validates request, manages high-level logic (networking, volumes, images) 3. dockerd delegates container operations to containerd via gRPC 4. containerd prepares the OCI bundle (rootfs + config.json) 5. containerd calls runc to create and start the container 6. runc sets up namespaces, cgroups, and rootfs, then starts the process 7. runc exits -- the container process runs directly under containerd
---
Image and Layer Model
Union Filesystem
Docker images use a union filesystem (typically overlay2) that stacks read-only layers on top of each other:
Container (running)
+---------------------------+
| Writable Container Layer | <-- Changes (writes, deletes) go here
+---------------------------+
| Layer 4: COPY app.js | \
+---------------------------+ |
| Layer 3: RUN npm install | > Read-only image layers
+---------------------------+ |
| Layer 2: COPY package.json | |
+---------------------------+ |
| Layer 1: FROM node:20-slim | /
+---------------------------+Key Layer Behaviors
- Each Dockerfile instruction that modifies the filesystem (RUN, COPY, ADD) creates one layer
- Layers are content-addressable -- identified by SHA256 digest of their contents
- Layers are shared across images -- if two images use the same base, the base layers exist only once on disk
- The writable container layer uses copy-on-write -- a file is copied from a lower layer to the writable layer only when modified
- Deleting a file in a higher layer creates a whiteout marker -- the file still exists in the lower layer but is hidden
- ALWAYS minimize layer count by combining related RUN commands with
&&
Image Identification
| Identifier | Format | Example |
|---|---|---|
| Repository + Tag | name:tag | nginx:1.25-alpine |
| Digest | name@sha256:... | nginx@sha256:a8560b... |
| Image ID | Short SHA256 | d1a364dc548d |
- Tags are mutable pointers --
nginx:latestcan point to different images over time - Digests are immutable -- ALWAYS use digests in production for reproducibility
- An image can have multiple tags pointing to the same digest
---
Build Context
The build context is the set of files sent to the Docker daemon when you run docker build.
How Build Context Works
1. CLI packages the build context directory into a tar archive 2. Tar archive is sent to the daemon (even if daemon is local) 3. COPY and ADD instructions reference files relative to the build context root 4. Files outside the build context are not accessible to the build
Build Context Rules
- The
.indocker build .specifies the build context directory - The Dockerfile location (
-f) is independent of the build context .dockerignorefilters files BEFORE sending to the daemon- ALWAYS exclude unnecessary files via
.dockerignoreto reduce context size and build time - Large contexts (>100MB) significantly slow down builds
Build Context Sources
| Source | Example | Notes |
|---|---|---|
| Local directory | docker build . | Most common |
| Git URL | docker build https://github.com/user/repo.git | Cloned by daemon |
| Tar archive | docker build - < archive.tar.gz | Extracted as context |
| stdin (Dockerfile only) | docker build - <<< "FROM alpine" | No file context available |
---
Container Lifecycle
State Diagram
docker create
|
v
+-----------+
| Created |
+-----------+
|
docker start
|
v
docker unpause +-----------+ docker pause
+--------------->| Running |<--------------+
| +-----------+ |
| | | |
| docker| |docker +--------+
| stop | | pause | Paused |
| | +-------------->+--------+
| v
| +-----------+
| | Stopped | (Exited)
| +-----------+
| |
| docker rm
| |
| v
| +-----------+
| | Removed |
| +-----------+Lifecycle States
| State | Description | Key Behavior |
|---|---|---|
| Created | Container exists but process has not started | Writable layer allocated, config set |
| Running | Main process is executing | Has PID, consumes resources, network active |
| Paused | Process suspended via cgroup freezer | Memory preserved, CPU released, no I/O |
| Stopped | Main process exited (exit code preserved) | Writable layer preserved, no resource usage |
| Removed | Container deleted | Writable layer deleted, anonymous volumes removed if --rm |
Command-to-Architecture Mapping
| Command | Docker Object Affected | What Happens |
|---|---|---|
docker build | Image | BuildKit executes Dockerfile, produces layered image |
docker pull | Image | containerd fetches layers from registry via OCI Distribution |
docker run | Container + (Image) | Pull if needed, create container, allocate writable layer, start process |
docker create | Container | Allocate writable layer, set config, do NOT start |
docker start | Container | containerd calls runc to start process |
docker stop | Container | Send SIGTERM, wait grace period, then SIGKILL |
docker kill | Container | Send signal immediately (default SIGKILL) |
docker rm | Container | Remove writable layer and metadata |
docker rmi | Image | Remove image layers (if not referenced by other images/containers) |
---
Container Isolation Model
Docker containers are isolated using Linux kernel primitives:
Namespaces (What the container can see)
| Namespace | Isolates | Effect |
|---|---|---|
| PID | Process IDs | Container sees only its own processes; PID 1 is the main process |
| Network | Network stack | Own IP address, ports, routing table, firewall rules |
| Mount | Filesystem | Own root filesystem via union mount |
| UTS | Hostname | Own hostname and domain name |
| IPC | Inter-process communication | Own shared memory, semaphores, message queues |
| User | UID/GID mapping | Root inside container maps to unprivileged user on host (optional) |
Cgroups (What the container can use)
| Resource | Control | CLI Flag |
|---|---|---|
| Memory | Hard limit, soft limit, swap | -m 512m, --memory-swap 1g |
| CPU | Share weight, core pinning, quota | --cpus 1.5, --cpuset-cpus 0-3 |
| PIDs | Maximum process count | --pids-limit 200 |
| Block I/O | Read/write bandwidth | --device-read-bps /dev/sda:1mb |
---
Decision Trees
When to Use Which Docker Object
Need persistent data?
YES --> Use a Volume (docker volume create)
Shared between containers? --> Named volume
Single container temp data? --> tmpfs mount
Host file access needed? --> Bind mount
NO --> Container writable layer is sufficient
Need container communication?
YES --> Use a Network (docker network create)
Same host? --> User-defined bridge
Multi-host? --> Overlay (requires Swarm)
Direct LAN? --> Macvlan
NO --> Use --network none
Need a reusable environment?
YES --> Build an Image (Dockerfile + docker build)
NO --> Use docker run with existing imageImage vs Container Decision
Is it a template (immutable, shareable, versioned)?
--> IMAGE: Build it, tag it, push it
Is it a running instance (has state, has PID, consumes resources)?
--> CONTAINER: Run it, stop it, remove it---
Reference Links
- references/concepts.md -- Docker object types, image layers, union filesystem details
- references/examples.md -- Architecture interaction examples, component diagrams
- references/anti-patterns.md -- Architectural mistakes and corrections
Official Sources
- https://docs.docker.com/get-started/docker-overview/
- https://docs.docker.com/engine/
- https://docs.docker.com/build/buildkit/
- https://docs.docker.com/engine/storage/
- https://docs.docker.com/engine/network/
- https://docs.docker.com/engine/security/
- https://opencontainers.org/
Docker Architecture Anti-Patterns
AP-001: Treating Containers as Persistent VMs
Problem: Treating containers like virtual machines -- logging in via docker exec, installing packages manually, making configuration changes, and expecting them to persist.
Why it fails: The writable container layer is ephemeral. When the container is removed, ALL changes are lost. Manual changes are unreproducible and invisible to other team members.
Correction:
- ALWAYS define the entire environment in a Dockerfile
- ALWAYS use
docker buildto create reproducible images - NEVER rely on manual
docker execchanges in production - Use volumes for data that must persist beyond the container lifecycle
---
AP-002: Using docker commit for Production Images
Problem: Making changes inside a running container and using docker commit to create new images.
# BAD
docker exec myapp apt-get install -y curl
docker commit myapp myapp:with-curlWhy it fails:
- No Dockerfile means no audit trail and no reproducibility
- Cannot rebuild the image automatically in CI/CD
- Layer content is opaque -- impossible to review what changed
- Accumulates unnecessary files and metadata over time
Correction:
- ALWAYS use a Dockerfile to define image contents
- ALWAYS build images via
docker buildordocker buildx build - Treat images as build artifacts, not mutable state
---
AP-003: Storing Data in the Container Layer
Problem: Writing application data (databases, uploads, logs) to the container's writable layer instead of a volume.
# BAD: Database data stored in container layer
docker run -d --name postgres postgres:16
# All data lost when container is removedWhy it fails:
- Container removal deletes the writable layer and all data
- Copy-on-write overhead degrades I/O performance for write-heavy workloads
- Data cannot be shared between containers
- Backup and migration become extremely difficult
Correction:
# GOOD: Named volume for persistent data
docker run -d --name postgres \
--mount source=pgdata,target=/var/lib/postgresql/data \
postgres:16- ALWAYS use named volumes for database data, uploads, and other persistent state
- ALWAYS use tmpfs mounts for sensitive temporary data (secrets, session files)
- NEVER store important data in the container writable layer
---
AP-004: Ignoring the Build Context
Problem: Running docker build without a .dockerignore file, sending gigabytes of unnecessary files to the daemon.
Why it fails:
- The entire build context directory is packaged as a tar and sent to the daemon
node_modules/(500MB+),.git/(50MB+), and build artifacts waste time and bandwidth- Large contexts cause multi-second delays on EVERY build, even with full cache hits
- Sensitive files (
.env, private keys) can accidentally end up in the image
Correction:
- ALWAYS create a
.dockerignorefile in every project with a Dockerfile - Exclude:
.git,node_modules,dist,build,*.md,.env,*.pem, IDE configs - Monitor context size with
docker buildoutput: "Sending build context to Docker daemon XX.XXB"
---
AP-005: Running All Processes as Root
Problem: Running the container process as root (the default) when root privileges are not required.
Why it fails:
- Container root maps to host root by default (unless user namespaces are enabled)
- A container escape vulnerability gives the attacker root access to the host
- File permission issues when mounting volumes -- files created by root in the container are owned by root on the host
Correction:
# ALWAYS create a non-root user
RUN groupadd -r appuser && useradd --no-log-init -r -g appuser appuser
USER appuser- ALWAYS add a
USERinstruction in the Dockerfile - ALWAYS assign explicit UID/GID for deterministic behavior
- NEVER use
--privilegedin production -- use specific--cap-addinstead
---
AP-006: Not Setting Resource Limits
Problem: Running containers without memory or CPU limits, allowing a single container to consume all host resources.
# BAD: No resource limits
docker run -d nginxWhy it fails:
- A memory leak in one container can trigger the OOM killer on the host, affecting ALL containers
- A CPU-intensive container can starve other containers of processing time
- A fork bomb can exhaust the PID table for the entire host
Correction:
# GOOD: Explicit resource limits
docker run -d \
-m 512m \
--cpus 1.0 \
--pids-limit 200 \
nginx- ALWAYS set memory limits (
-m) in production - ALWAYS set CPU limits (
--cpus) in production - ALWAYS set PID limits (
--pids-limit) to prevent fork bombs
---
AP-007: Using the Default Bridge Network
Problem: Relying on the default bridge network for container communication.
Why it fails:
- No automatic DNS resolution -- containers can only reach each other by IP address
- IP addresses change on container restart -- hardcoded IPs break
- ALL containers on the default bridge can reach each other -- no isolation
- The legacy
--linkflag is deprecated and unreliable
Correction:
# GOOD: User-defined bridge with automatic DNS
docker network create mynet
docker run -d --name web --network mynet nginx
docker run -d --name api --network mynet myapi
# api can reach web as "web" via DNS- ALWAYS create user-defined bridge networks
- NEVER use
--linkfor container communication - NEVER hardcode container IP addresses
---
AP-008: Monolithic Containers
Problem: Running multiple services (web server + database + cache) inside a single container.
Why it fails:
- Cannot scale services independently
- Failure of one service kills all services in the container
- Cannot update one service without restarting all others
- Signal handling becomes complex with multiple PID 1 candidates
- Violates the single-responsibility principle for containers
Correction:
- ALWAYS run one primary process per container
- Use Docker Compose or orchestration to manage multi-service applications
- Connect services via Docker networks
- Use shared volumes for inter-service data when needed
---
AP-009: Conflating Image Tags with Immutability
Problem: Assuming that nginx:1.25 will always refer to the exact same image.
Why it fails:
- Tags are mutable pointers -- the registry maintainer can push a new image under the same tag
latestis especially dangerous -- it changes with every new release- Security patches often update tagged images without changing the tag
- Two
docker pullcommands at different times can return different images for the same tag
Correction:
# Pin by digest for production
FROM nginx:1.25@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c- ALWAYS use digest pinning for production Dockerfiles
- NEVER use
latestin production - Use tags for development convenience, digests for production determinism
---
AP-010: Exposing the Docker Daemon API Without TLS
Problem: Binding the Docker daemon to a TCP port without TLS encryption and authentication.
{
"hosts": ["tcp://0.0.0.0:2375"]
}Why it fails:
- Docker API access is equivalent to root access on the host
- Anyone who can reach the TCP port can create privileged containers
- Attackers can mount the host root filesystem and gain full control
- Cryptocurrency mining botnets actively scan for exposed Docker APIs
Correction:
- NEVER expose the Docker daemon on TCP without TLS mutual authentication
- ALWAYS use Unix socket (
/var/run/docker.sock) for local access - ALWAYS use SSH tunneling for remote access:
DOCKER_HOST=ssh://user@host - If TCP is required, ALWAYS configure TLS with client certificates
---
AP-011: Misunderstanding Layer Caching
Problem: Placing frequently-changing files before expensive operations in the Dockerfile.
# BAD: Any source code change invalidates the npm install cache
FROM node:20
WORKDIR /app
COPY . .
RUN npm installWhy it fails:
- Docker layer caching is sequential -- once a layer's cache is invalidated, ALL subsequent layers must rebuild
- Copying source code (which changes frequently) before installing dependencies (which change rarely) means
npm installruns on every build
Correction:
# GOOD: Dependencies cached separately from source code
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .- ALWAYS order Dockerfile instructions from least to most frequently changed
- ALWAYS separate dependency installation from source code copying
- Use
--mount=type=cachefor package manager caches
---
AP-012: Using docker save/load Instead of a Registry
Problem: Distributing images by saving to tar files and loading on target machines.
Why it fails:
- No version control or tagging at the distribution level
- No layer sharing between images (full image transferred every time)
- No security scanning or vulnerability assessment in the pipeline
- Manual process prone to errors and stale images
Correction:
- ALWAYS use a container registry (Docker Hub, GitHub Container Registry, AWS ECR) for image distribution
- Use
docker pushanddocker pullfor image transfer - Implement CI/CD pipelines that build, scan, and push images automatically
- Reserve
docker save/loadfor air-gapped environments only
Docker Architecture Concepts
Docker Object Types in Detail
Images
An image is an immutable, ordered collection of read-only filesystem layers plus metadata (config, environment, entrypoint, labels).
Key properties:
- Images are built from a Dockerfile using
docker build - Each layer represents one filesystem change (file added, modified, or deleted)
- Layers are content-addressable -- identified by the SHA256 hash of their contents
- Layers are shared between images -- common base layers exist only once on disk
- Images are distributed via registries using the OCI Distribution Specification
- An image manifest lists all layers and the image config
Image manifest structure:
Image Manifest
├── Config (JSON)
│ ├── Environment variables
│ ├── Entrypoint / CMD
│ ├── Working directory
│ ├── User
│ ├── Exposed ports
│ ├── Labels
│ └── Layer diff IDs (ordered)
├── Layers (ordered list)
│ ├── Layer 1 digest + size
│ ├── Layer 2 digest + size
│ └── Layer N digest + size
└── Metadata
├── Schema version
├── Media type
└── Platform (os/arch)Multi-platform images use a manifest list (also called an index) that points to platform-specific manifests:
Manifest List (Index)
├── linux/amd64 --> Manifest A
├── linux/arm64 --> Manifest B
└── linux/arm/v7 --> Manifest CContainers
A container is a runnable instance of an image with its own writable layer, network stack, process space, and configuration.
Key properties:
- Created from an image using
docker runordocker create - Has a thin writable layer on top of the image's read-only layers
- The writable layer is deleted when the container is removed
- Multiple containers can share the same image without duplication
- Each container has its own isolated namespaces and cgroup limits
- Container state (created/running/paused/stopped) is tracked by the daemon
Container filesystem model:
+-----------------------------+
| Container Writable Layer | <-- Copy-on-write: files copied here only when modified
+-----------------------------+
| Image Layer N (read-only) |
+-----------------------------+
| ... |
+-----------------------------+
| Image Layer 1 (read-only) |
+-----------------------------+When a container reads a file, Docker searches layers top-down and returns the first match. When a container modifies a file, the file is first copied from the read-only layer to the writable layer (copy-on-write), then modified in place.
Networks
A Docker network provides isolated communication channels between containers.
Key properties:
- Containers on the same user-defined network can resolve each other by name (automatic DNS)
- Containers on the default bridge network can ONLY communicate via IP address
- A container can connect to multiple networks simultaneously
- Network isolation means containers on different networks cannot communicate
- ALWAYS use user-defined bridge networks, not the default bridge
Network types:
| Type | Isolation | DNS | Use Case |
|---|---|---|---|
| bridge (user-defined) | Per-network | Automatic | Standard container communication |
| bridge (default) | Weak | None | Legacy, avoid |
| host | None | Host DNS | Performance-critical, no port mapping needed |
| none | Complete | None | Fully isolated containers |
| overlay | Multi-host | Automatic | Swarm services across hosts |
| macvlan | Appears as physical device | Network-level | Direct LAN integration |
Volumes
A volume is a Docker-managed persistent storage mechanism that exists outside the container's union filesystem.
Key properties:
- Volumes persist independently of any container lifecycle
- Stored in
/var/lib/docker/volumes/on the host (by default) - Can be shared between multiple containers simultaneously
- Support different drivers (local, NFS, cloud storage plugins)
- Named volumes are explicitly created and referenced by name
- Anonymous volumes are created automatically and identified by a random hash
Storage comparison:
| Storage Type | Managed By | Persists After docker rm | Shared Between Containers | Performance |
|---|---|---|---|---|
| Container writable layer | Docker (overlay2) | No | No | Good (copy-on-write overhead) |
| Named volume | Docker | Yes | Yes | Best (direct mount) |
| Bind mount | Host filesystem | Yes (host file) | Yes | Best (direct access) |
| tmpfs | Kernel (memory) | No | No | Fastest (RAM) |
---
Union Filesystem (overlay2)
Docker Engine 24+ uses overlay2 as the default storage driver. overlay2 implements a union filesystem that merges multiple directories into a single coherent view.
overlay2 Architecture
Container View (merged) What the container process sees
|
+-- merged/ Unified view of all layers
|
+-- diff/ Container's writable layer (upperdir)
|
+-- work/ Internal overlay2 work directory
|
+-- lower Pointer to read-only image layers (lowerdir)
|
+-- Layer N
+-- ...
+-- Layer 1Copy-on-Write (CoW) Mechanics
| Operation | What Happens |
|---|---|
| Read file | Search layers top-down, return first match |
| Create new file | Write directly to the writable (upper) layer |
| Modify existing file | Copy entire file from lower layer to upper layer, then modify |
| Delete file | Create a whiteout file in upper layer (character device 0:0) |
| Delete directory | Create an opaque whiteout by setting trusted.overlay.opaque=y xattr |
Performance Implications
- First write to an existing file incurs a copy-up cost (entire file copied to writable layer)
- Large files (databases, logs) should ALWAYS use volumes to avoid copy-on-write overhead
- The more layers an image has, the deeper the search path for file lookups
- overlay2 supports up to 128 lower layers
Layer Sharing
Image A: [Base] [Layer 1] [Layer 2]
Image B: [Base] [Layer 1] [Layer 3]
On disk: [Base] [Layer 1] [Layer 2] [Layer 3]
^
Layer 1 and Base stored only onceThis sharing means pulling a new image that shares a base with an existing image downloads only the new layers. docker system df -v shows the "Shared Size" for each image.
---
Docker Daemon (dockerd)
The Docker daemon is the central management process that:
1. Exposes the Docker API -- REST API over Unix socket (/var/run/docker.sock) or TCP 2. Manages images -- Build, pull, push, tag, inspect, remove 3. Manages containers -- Create, start, stop, remove, inspect 4. Manages networks -- Create, connect, disconnect, remove 5. Manages volumes -- Create, inspect, remove 6. Delegates runtime operations to containerd via gRPC
Daemon Configuration
Configuration is set in /etc/docker/daemon.json:
{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"default-address-pools": [
{"base": "172.17.0.0/16", "size": 24}
]
}NEVER set the same option in both daemon.json and CLI flags -- the daemon will fail to start with a conflict error.
---
containerd
containerd is the container runtime supervisor that sits between the Docker daemon and runc:
1. Pulls and pushes images using the OCI Distribution Specification 2. Manages image layer snapshots using the overlay2 snapshotter 3. Prepares the OCI bundle (rootfs + runtime config) for runc 4. Supervises container processes after runc exits 5. Provides the shim process that keeps the container running independently of containerd restarts
The Shim Process
When containerd starts a container, it creates a shim process (containerd-shim-runc-v2) that:
- Becomes the parent of the container process
- Keeps STDIO open for the container
- Reports exit status back to containerd
- Allows containerd to restart without affecting running containers
---
runc
runc is the OCI reference runtime -- a lightweight binary that creates and starts containers:
1. Receives an OCI bundle (rootfs directory + config.json) 2. Creates Linux namespaces (PID, network, mount, UTS, IPC, user) 3. Configures cgroups for resource limits 4. Sets up the root filesystem using pivot_root or chroot 5. Starts the container process 6. Exits -- runc does NOT supervise the container; the shim takes over
runc is replaceable with any OCI-compliant runtime (e.g., crun, kata-containers, gVisor/runsc).
---
OCI Standards
The Open Container Initiative (OCI) defines three specifications that Docker follows:
OCI Image Specification
- Defines the format for container images
- An image is a manifest, a config, and an ordered set of filesystem layers
- Layers are tar archives, optionally compressed (gzip, zstd)
- Content-addressable storage using SHA256 digests
OCI Runtime Specification
- Defines how to run a "filesystem bundle"
- A bundle is a directory containing a
config.jsonand arootfs/directory config.jsonspecifies: process, root filesystem, mounts, namespaces, cgroups, capabilities- Defines lifecycle operations: create, start, kill, delete
- Defines standard hooks: prestart, poststart, poststop
OCI Distribution Specification
- Defines the API for distributing container images
- Registry endpoints:
/v2/,/v2/<name>/manifests/<ref>,/v2/<name>/blobs/<digest> - Supports content discovery, pull, push, and deletion
- Used by Docker Hub, GitHub Container Registry, AWS ECR, and all OCI-compliant registries
Docker Architecture Examples
Component Interaction: docker run
When you execute docker run -d --name web -p 8080:80 nginx:1.25, the following sequence occurs:
Step 1: CLI --> Daemon (REST API)
docker run -d --name web -p 8080:80 nginx:1.25
POST /v1.45/containers/create
POST /v1.45/containers/{id}/start
Step 2: Daemon checks local image store
Image nginx:1.25 present?
NO --> Pull from registry (Steps 2a-2c)
YES --> Skip to Step 3
Step 2a: Daemon --> containerd (gRPC)
Pull image nginx:1.25
Step 2b: containerd --> Registry (HTTPS)
GET /v2/library/nginx/manifests/1.25
GET /v2/library/nginx/blobs/sha256:... (for each layer)
Step 2c: containerd stores layers
Snapshotter prepares overlay2 mount points
Step 3: Daemon creates container metadata
- Assigns container ID
- Registers name "web"
- Configures port mapping 8080:80
- Allocates writable layer
Step 4: Daemon --> containerd (gRPC)
Create container with OCI bundle
Step 5: containerd --> runc
Create namespaces, cgroups, rootfs
Start nginx master process
Step 6: runc exits, shim supervises
nginx is PID 1 inside the container
shim is the parent process on the hostComponent Interaction: docker build
When you execute docker build -t myapp:v1 .:
Step 1: CLI packages build context
- Reads .dockerignore
- Creates tar archive of context directory
- Sends tar to daemon via REST API
Step 2: Daemon delegates to BuildKit
- Parses Dockerfile
- Creates build graph (DAG of instructions)
- Identifies parallelizable stages
Step 3: BuildKit executes instructions
For each instruction:
- Check layer cache (content-addressable)
- If cache hit --> reuse layer
- If cache miss --> execute instruction, create new layer
Step 4: BuildKit produces image
- Assembles layers into image manifest
- Stores in local image store
- Tags as myapp:v1Layer Creation Walkthrough
Given this Dockerfile:
FROM alpine:3.21 # Layer 1: Base image layers
RUN apk add --no-cache curl # Layer 2: +curl binary and libs
COPY config.json /app/config.json # Layer 3: +config file
RUN mkdir -p /data && chown 1001 /data # Layer 4: +data directory
USER 1001 # No layer (metadata only)
CMD ["myapp"] # No layer (metadata only)Layer analysis:
Layer 1: alpine base [~6 MB] -- shared with all alpine-based images
Layer 2: +curl [~2 MB] -- added files from apk install
Layer 3: +config.json [~1 KB] -- single file added
Layer 4: +/data dir [~0 KB] -- empty directory + ownership change
Image config (not a layer):
- USER: 1001
- CMD: ["myapp"]
- ENV: inherited from alpineInstructions that do NOT create layers:
FROM(references existing layers)CMD,ENTRYPOINT(metadata)ENV,ARG(metadata, though ENV creates a cache checkpoint)EXPOSE,VOLUME,LABEL(metadata)USER,WORKDIR(metadata)STOPSIGNAL,SHELL,HEALTHCHECK(metadata)
Instructions that create layers:
RUN(executes command, captures filesystem diff)COPY(adds files from build context)ADD(adds files from context, URL, or Git repo)
Container Isolation Example
Two containers from the same image have completely isolated environments:
Host System
├── dockerd (daemon)
├── containerd
│
├── Container A (from nginx:1.25)
│ ├── PID namespace: PID 1 = nginx master
│ ├── Network: 172.17.0.2, port 80
│ ├── Mount:
│ │ ├── [read-only] Image layers (shared with B)
│ │ └── [writable] Container A's own layer
│ ├── Hostname: container-a-id
│ └── Cgroups: 512MB memory, 1.0 CPU
│
├── Container B (from nginx:1.25)
│ ├── PID namespace: PID 1 = nginx master (different process!)
│ ├── Network: 172.17.0.3, port 80
│ ├── Mount:
│ │ ├── [read-only] Image layers (shared with A)
│ │ └── [writable] Container B's own layer
│ ├── Hostname: container-b-id
│ └── Cgroups: 256MB memory, 0.5 CPU
│
└── Shared Resources
└── Image layers on disk (stored once, mounted read-only by both)Key observations:
- Both containers share the same read-only image layers (zero disk duplication)
- Each container has its own writable layer (isolated writes)
- Each container has its own PID 1 (different nginx processes)
- Each container has its own IP address and network stack
- Resource limits are independent per container
Network Architecture Example
Host Network Stack
│
├── docker0 (default bridge: 172.17.0.0/16)
│ ├── veth-a ←→ Container A eth0 (172.17.0.2)
│ └── veth-b ←→ Container B eth0 (172.17.0.3)
│ (NO DNS resolution between A and B)
│
├── br-mynet (user-defined bridge: 172.18.0.0/16)
│ ├── veth-c ←→ Container C eth0 (172.18.0.2)
│ └── veth-d ←→ Container D eth0 (172.18.0.3)
│ (DNS: C can reach D as "container-d-name")
│
└── iptables rules
├── NAT: Container outbound traffic masqueraded as host IP
├── FORWARD: Inter-container traffic on same bridge allowed
└── DNAT: Published ports (-p 8080:80) forwarded to containerVolume Architecture Example
Host Filesystem
│
├── /var/lib/docker/volumes/
│ ├── pgdata/
│ │ └── _data/ <-- Named volume "pgdata"
│ │ ├── base/
│ │ ├── global/
│ │ └── pg_wal/
│ └── app-logs/
│ └── _data/ <-- Named volume "app-logs"
│ └── app.log
│
├── Container: postgres
│ └── /var/lib/postgresql/data --> mounted from pgdata volume
│ (reads/writes go directly to host, bypassing overlay2)
│
└── Container: app
├── /var/log/app --> mounted from app-logs volume
└── /app/ --> overlay2 (container writable layer)Key observations:
- Volume data bypasses the union filesystem entirely (no copy-on-write overhead)
- Volume data persists when the container is removed
- Multiple containers can mount the same volume simultaneously
Build Context Transfer
Project Directory
├── src/
│ ├── main.go (10 KB) -- included
│ └── utils.go (5 KB) -- included
├── tests/
│ └── main_test.go (8 KB) -- excluded by .dockerignore
├── node_modules/ (500 MB) -- excluded by .dockerignore
├── .git/ (50 MB) -- excluded by .dockerignore
├── Dockerfile (1 KB) -- excluded by .dockerignore
├── .dockerignore (1 KB) -- processed first, not sent
└── go.mod (1 KB) -- included
Without .dockerignore: ~560 MB sent to daemon
With .dockerignore: ~16 KB sent to daemonThe .dockerignore file:
.git
node_modules
tests
Dockerfile
.dockerignore
*.mdALWAYS create a .dockerignore file. The build context is sent as a tar archive over the Docker API before any instruction executes. A 500MB context adds seconds to every build, even with full cache hits.
Container Lifecycle Practical Example
# 1. CREATE: Allocate writable layer, set config, do not start
docker create --name myapp -p 8080:80 nginx:1.25
# State: Created | PID: none | Network: allocated but inactive
# 2. START: Execute the main process
docker start myapp
# State: Running | PID: active | Network: active, port 8080 mapped
# 3. PAUSE: Freeze all processes via cgroup freezer
docker pause myapp
# State: Paused | PID: frozen | Network: connections stall, no new responses
# 4. UNPAUSE: Resume all processes
docker unpause myapp
# State: Running | PID: active | Network: resumes normally
# 5. STOP: Send SIGTERM, wait 10s, then SIGKILL
docker stop myapp
# State: Stopped (Exited 0) | PID: none | Network: released
# Writable layer: PRESERVED (can be started again)
# 6. START again: Resume from stopped state
docker start myapp
# State: Running | PID: new process | Network: new IP possible
# 7. STOP and REMOVE: Delete the container
docker stop myapp
docker rm myapp
# State: Removed | Writable layer: DELETED | Anonymous volumes: DELETEDOCI Bundle Structure
When containerd prepares a container for runc, it creates an OCI bundle:
/run/containerd/io.containerd.runtime.v2.task/<namespace>/<container-id>/
├── config.json <-- OCI Runtime Spec configuration
│ ├── process (command, args, env, cwd, user)
│ ├── root (path to rootfs, readonly flag)
│ ├── mounts (procfs, sysfs, devpts, tmpfs, volumes)
│ ├── linux
│ │ ├── namespaces (pid, network, mount, uts, ipc)
│ │ ├── resources (cgroups: memory, cpu, pids)
│ │ └── seccomp (syscall filter profile)
│ └── hooks (prestart, poststart, poststop)
└── rootfs/ <-- Union mount of image layers + writable layer
├── bin/
├── etc/
├── usr/
└── ...