Docker Images vs Containers: Understanding the Difference

A technical guide to Docker images vs containers: OCI specifications, OverlayFS storage layers, copy-on-write mechanics, lifecycle states, and persistence models.

November 12, 202518 min readOpenStair Engineering
Illustration of container images and running application containers in a deployment environment

Docker Images vs Containers: Understanding the Difference

When software engineers first learn containerization, instructors often introduce a convenient analogy drawn from object-oriented programming: "an image is a class, and a container is an instantiated object." Alternatively, teams hear that "an image is a frozen blueprint, and a container is the running house."

While these metaphors help beginners grasp the conceptual separation between a static template and an active instance, they obscure the systems-level engineering reality. Treating a container as a "running image" or a "lightweight virtual machine" leads directly to production anti-patterns: database storage placed on ephemeral storage drivers, multi-gigabyte build artifacts deployed across registries, applications failing to terminate cleanly during zero-downtime rollouts, and subtle performance bottlenecks caused by union filesystem copy-up operations.

To design resilient continuous integration pipelines, optimize container startup latency, and prevent data loss, engineers must understand the concrete mechanisms that differentiate Docker images from containers.

At the operating system level, a Docker Image is a static, immutable, cryptographically verifiable collection of read-only tar archives and JSON descriptors defined by the Open Container Initiative (OCI) Image Specification. A Docker Container is a standard host Linux process governed by kernel namespaces and control groups (cgroups), executing on top of an ephemeral, copy-on-write union filesystem mount.

This guide examines the structural, architectural, and operational boundaries separating images from containers. It walks through OCI manifest structures, OverlayFS storage internals, container lifecycle state machines, exit code semantics, and persistence models.


1. Definitional Boundaries: Static Artifact vs. Runtime Process

The distinction between an image and a container begins with how each entity exists on disk, in memory, and within the host operating system kernel.

+-----------------------------------------------------------------------------+
|                           OCI CONTAINER IMAGE                               |
|                         (Static Binary Artifact)                            |
|                                                                             |
|  [ Image Index / Fat Manifest ] -> Multi-Arch Resolution (amd64 / arm64)   |
|         │                                                                   |
|         ▼                                                                   |
|  [ Image Manifest JSON ]       -> Content-Addressable Cryptographic Digest  |
|         │                                                                   |
|         ├──> [ Config JSON ]   -> Env, Entrypoint, DiffIDs, Architecture    |
|         │                                                                   |
|         └──> [ Layer Tarballs] -> Layer 1 (Base OS)       [Immutable / RO]  |
|                                -> Layer 2 (Dependencies)  [Immutable / RO]  |
|                                -> Layer 3 (App Binary)    [Immutable / RO]  |
+-----------------------------------------------------------------------------+
                                       │
                         docker run / containerd / runc
                                       │
                                       ▼
+-----------------------------------------------------------------------------+
|                            DOCKER CONTAINER                                 |
|                         (Dynamic Host Process)                              |
|                                                                             |
|  [ Linux Host Kernel ]                                                      |
|         ├── Namespaces: PID (sees PID 1), NET (veth0), MNT, IPC, UTS, USER  |
|         └── Control Groups (cgroups v2): memory.max, cpu.max, io.weight     |
|                                                                             |
|  [ Storage: OverlayFS Mount ]                                               |
|         ├── Upperdir (Read-Write Layer): Ephemeral process state mutations  |
|         ├── Workdir: Internal atomic scratchpad                             |
|         └── Lowerdir: Stacked read-only image layers (Layer 1 + 2 + 3)      |
|                                                                             |
|  [ Process Execution ]                                                      |
|         └── Main Application Process (PID 1 inside container PID namespace) |
+-----------------------------------------------------------------------------+

The Anatomy of an OCI Image

A Docker image does not contain a running kernel, a virtualized BIOS, or active processes. It is a packaged, content-addressable filesystem bundle and configuration contract. Under the Open Container Initiative (OCI) Image Specification, an image consists of four core elements:

  1. The Image Manifest (application/vnd.oci.image.manifest.v1+json): A JSON document that defines the image's structure. It contains references (cryptographic SHA-256 digests and byte sizes) to a single configuration object and an ordered array of layer tarballs.
  2. The Image Configuration JSON (application/vnd.oci.image.config.v1+json): A document specifying execution parameters: default environment variables (ENV), working directory (WORKDIR), process entry points (ENTRYPOINT, CMD), exposed ports (EXPOSE), and the ordered array of uncompressed filesystem layer diff IDs (rootfs.diff_ids).
  3. Layer Diff Archives (application/vnd.oci.image.layer.v1.tar+gzip): Each image layer represents a tarball containing only the filesystem differences (additions, modifications, and deletions) introduced by a specific build instruction (RUN, COPY, ADD). Every layer is content-addressable, meaning its identifier is the cryptographic SHA-256 hash of its compressed payload.
  4. The OCI Image Index (application/vnd.oci.image.index.v1+json): Commonly known as a "fat manifest" or multi-architecture manifest list. An Image Index maps a single repository tag (such as golang:1.24-alpine) to multiple distinct platform manifests (such as linux/amd64, linux/arm64/v8, or linux/s390x). When a container runtime pulls an image, it queries the index and retrieves only the manifest and layers matching the host machine's target architecture.

Because every component is content-addressed by its cryptographic hash, images are strictly immutable. You cannot edit an existing layer in place. Modifying a single byte inside a layer produces a completely different SHA-256 digest, which breaks the hash reference in the manifest, creating a new image.

The Anatomy of a Container

A container is not a self-contained operating system; it is a standard Linux process running directly on the host machine's shared kernel.

When you run docker run, the Docker daemon delegates execution to a container runtime manager (containerd), which invokes a low-level OCI runtime (runc). The runtime does not launch a hypervisor. Instead, it makes native Linux system calls to construct an isolated execution jail:

  • Namespaces (clone(), unshare(), setns() flags): The kernel isolates what the process can view. The PID namespace makes the containerized application believe it is PID 1 on the system, while the host process table sees it as an ordinary unprivileged process (e.g., PID 28491). The NET namespace provides dedicated virtual network interfaces, routing tables, and firewall chains. The MNT namespace isolates the filesystem mount points.
  • Control Groups (cgroups v2): The kernel enforces resource quotas on what the process can consume. Resource limits configured via flags such as --memory="512m" and --cpus="2.0" write boundaries into /sys/fs/cgroup/, capping physical RAM consumption and throttling CPU scheduler periods.
  • Security Profiles (Seccomp and AppArmor/SELinux): Filter and restrict which Linux system calls the containerized process can invoke, preventing unprivileged processes from issuing sensitive kernel operations.
  • Union Filesystem Mount: A writable storage overlay instantiated on top of the image's immutable lower layers, giving the process a unified root filesystem (/) where it can read binaries and write runtime temporary files.

2. Storage Layer Internals: OverlayFS and Copy-on-Write (CoW)

The fundamental technical bridge between a static image and a dynamic container is the storage driver. Modern Linux container runtimes almost universally use OverlayFS (managed in Docker via the overlay2 storage driver).

OverlayFS is a union filesystem. Rather than copying hundreds of megabytes of operating system files whenever a new container is instantiated, OverlayFS presents multiple separate host directories as a single unified directory tree.

Unified Container Filesystem View (/bin, /etc, /app, /tmp)
▲
│ [Merged Mount: /var/lib/docker/overlay2/<merged_id>/merged]
│
├── Upperdir [Read-Write Container Layer]
│   Location: /var/lib/docker/overlay2/<id>/diff
│   Contents: New files created at runtime, modified files (copied up), whiteouts
│
├── Workdir [Atomic Scratchpad]
│   Location: /var/lib/docker/overlay2/<id>/work
│   Contents: Staging area for atomic copy-up and directory deletion operations
│
└── Lowerdir [Read-Only Image Layers]
    Location: /var/lib/docker/overlay2/<layer-sha>/diff (colon-separated stack)
    Contents: Immutable base OS packages, application dependencies, compiled code

The Four Directories of OverlayFS

When Docker launches a container, it requests an OverlayFS mount with four specific directory roles:

  1. lowerdir (Read-Only): A colon-separated list of directories pointing to the unpacked diff directories of the image layers. These directories reside on the host filesystem under /var/lib/docker/overlay2/. They are mounted with read-only permissions. If ten containers run simultaneously from the same base image, all ten containers share the identical host lowerdir paths in memory and on disk.
  2. upperdir (Read-Write): A dedicated, writable directory created exclusively for this specific container instance. Any file created, modified, or deleted while the container is running takes place physically inside this directory.
  3. workdir (Internal Scratchpad): An auxiliary directory required by the Linux kernel OverlayFS implementation. It must reside on the same host filesystem as upperdir. OverlayFS uses workdir to stage files during copy-up operations before atomically moving them into upperdir, preventing partial file corruptions if a write is interrupted.
  4. merged (Unified Mountpoint): The directory where the kernel exposes the union view. This is the mount target passed into pivot_root or chroot during container initialization. From inside the container, processes see merged as the root filesystem (/).

The Copy-on-Write (CoW) Lifecycle

The interaction between the immutable lowerdir and the ephemeral upperdir is governed by Copy-on-Write semantics:

File Reads

When a containerized process opens a file for reading:

  • The kernel searches the layers from top to bottom.
  • It checks upperdir first. If the file exists there (meaning it was created or modified by the container), the kernel serves that version.
  • If the file is not in upperdir, the kernel searches through the lowerdir stack, starting with the topmost image layer and moving downward toward the base layer.
  • The first matching file found is opened directly.

File Modifications (The Copy-Up Penalty)

When a process opens an existing file that belongs to a read-only lowerdir layer with write flags (O_WRONLY or O_RDWR):

  • The file does not yet exist in upperdir.
  • Before the write system call can proceed, OverlayFS performs a copy-up operation: the entire file is copied from the lowerdir layer up into upperdir.
  • Once the copy completes, the process performs its write operation on the newly created replica inside upperdir. The original file in lowerdir remains pristine and unchanged.
  • Subsequent writes to this file occur directly within upperdir without additional copy overhead.

[!WARNING] The Copy-Up Latency Spike: If a container opens a 2 GB SQLite database or application archive from an image layer to append a single 4-byte record, OverlayFS must copy all 2 GB from lowerdir to upperdir before executing the append. This introduces substantial write latency spikes and consumes duplicate disk space.

File Deletions (Whiteouts)

Because the layers in lowerdir are mounted read-only, a container process cannot physically erase files from an underlying image layer. When a container executes rm /etc/issue:

  • OverlayFS intercepts the unlink() system call.
  • Instead of deleting the file in lowerdir, OverlayFS creates a special character device whiteout in upperdir with major and minor device numbers set to 0,0 (c 0 0), named identically to the deleted file.
  • When the kernel scans the filesystem, encountering this 0,0 character device in upperdir causes it to mask and hide the corresponding file in lowerdir. The file appears deleted to all container processes.
  • Similarly, if an entire directory inherited from lowerdir is removed, OverlayFS creates an opaque directory in upperdir marked with the extended attribute trusted.overlay.opaque="y".

3. Storage Topologies: Ephemeral Upperdir vs. Native Persistence

Because the container's writable layer (upperdir) uses OverlayFS, it has distinct architectural trade-offs compared to persistent host storage. Engineers must distinguish between the ephemeral container layer and dedicated volume mounts.

+-----------------------------------------------------------------------------+
|                          STORAGE ARCHITECTURES                              |
+-----------------------------------------------------------------------------+
|                                                                             |
| 1. Container Writable Layer (OverlayFS)                                     |
|    - Path: /var/lib/docker/overlay2/<id>/diff                               |
|    - Lifecycle: Ephemeral. Destroyed when container is removed (docker rm)  |
|    - Performance: CoW penalty on first write, dentry lookup overhead       |
|    - Best For: Ephemeral process scratch files, local log buffers           |
|                                                                             |
| 2. Docker Named Volumes                                                     |
|    - Path: /var/lib/docker/volumes/<vol_name>/_data                         |
|    - Lifecycle: Decoupled. Survives container destruction and upgrades     |
|    - Performance: Native host ext4/xfs speed; bypasses OverlayFS driver     |
|    - Best For: Databases (Postgres, MySQL), persistent application state   |
|                                                                             |
| 3. Host Bind Mounts                                                         |
|    - Path: Explicit host directory (e.g., /opt/data, ~/project)             |
|    - Lifecycle: Tied to host filesystem. Completely external to Docker      |
|    - Performance: Native host speed; bypasses OverlayFS driver              |
|    - Best For: Local development source code sharing, host config files     |
|                                                                             |
| 4. Memory-Backed Mounts (tmpfs)                                             |
|    - Path: Host volatile system RAM                                         |
|    - Lifecycle: Destroyed immediately when container stops                  |
|    - Performance: Ultra-low latency memory bus speed; zero disk I/O         |
|    - Best For: Ephemeral secrets, decryption keys, high-frequency caches   |
+-----------------------------------------------------------------------------+

Why the Writable Layer Degrades I/O Performance

Writing high-frequency application data (such as database transaction logs or active message queues) directly to a container's root filesystem causes severe performance degradation due to three architectural factors:

  1. Directory Lookup Traversal: Every time the kernel resolves a file path across an OverlayFS mount, it must traverse multiple stacked lowerdir layers. As the number of image layers increases, directory cache (dentry) lookups incur additional CPU overhead compared to a flat ext4 or XFS filesystem.
  2. First-Write Copy-Up Overhead: As detailed above, modifying any file bundled into the image triggers a synchronous byte-for-byte copy to upperdir.
  3. Page Cache Duplication: When an unmodified file in lowerdir is read, its data resides in the Linux kernel's page cache. Once that file is copied up to upperdir and modified, it acquires a distinct inode. The kernel must allocate independent page cache entries for the modified file, increasing memory pressure on the host.

Storage Topology Comparison Matrix

| Storage Mechanism | Underlying Driver | Write Performance | Persistence & Lifecycle | Host Coupling | Recommended Use Cases | | :--- | :--- | :--- | :--- | :--- | :--- | | Container Writable Layer | OverlayFS (overlay2) | Degraded (CoW copy-up latency, dentry lookup overhead) | Ephemeral: destroyed permanently on docker rm | Managed by Docker in /var/lib/docker/overlay2 | Transient command logs, small temporary configuration tweaks, container scratch space. | | Docker Named Volume | Native host filesystem (ext4, xfs, or volume plugin) | Maximum (native raw filesystem speed; bypasses storage driver) | Independent: persists indefinitely until explicitly deleted via docker volume rm | Managed by Docker in /var/lib/docker/volumes/ | Production databases (PostgreSQL, MongoDB), file uploads, stateful application data. | | Host Bind Mount | Native host filesystem | Maximum (native raw filesystem speed; bypasses storage driver) | Controlled by host filesystem lifecycle | High coupling: dependent on exact host directory structure and UID permissions | Local development live-reload, mounting host configuration files (e.g., /etc/ssl/certs). | | tmpfs Mount | Linux tmpfs (host RAM) | Maximum (memory bus bandwidth, zero disk writes) | Non-persistent: wiped from memory the instant container stops | Purely in-memory; no disk footprint | Ephemeral cryptographic secrets, session tokens, security-sensitive decryption keys. |


4. Container Lifecycle and State Machine

A Docker image is static and stateless. Once built, it sits dormant in a local store or remote registry. A container, in contrast, is an active state machine managed by the container runtime.

The Five Primary Container States

A container transitions through five core states during its life cycle:

                 +-------------------+
                 |    NON-EXISTENT   |
                 +-------------------+
                           │
                 docker create / docker run
                           │
                           ▼
                 +-------------------+
        ┌───────>|      CREATED      |
        │        +-------------------+
        │                  │
        │             docker start
        │                  │
        │                  ▼
        │        +-------------------+
        │        |      RUNNING      |<────────┐
        │        +-------------------+         │
        │             │        │               │
        │  docker stop│        │docker pause   │docker unpause
        │  (or crash) │        │               │
        │             │        ▼               │
        │             │  +-------------------+ │
        │             │  |      PAUSED       |─┘
        │             │  | (cgroup freezer)  |
        │             │  +-------------------+
        │             ▼
        │        +-------------------+
        └────────|  STOPPED (EXITED) |
     docker start+-------------------+
                           │
                       docker rm
                           │
                           ▼
                 +-------------------+
                 |      DELETED      |
                 +-------------------+
  1. Created: The container object has been instantiated. Docker has assigned a unique 64-character hexadecimal container ID, created the dedicated upperdir and workdir storage paths on the host, parsed environment variables, and configured network settings. However, no host process has been spawned.
  2. Running: The container runtime (runc) has invoked clone() with isolated namespace flags, mounted the rootfs via OverlayFS, applied cgroups limits, and executed the image's ENTRYPOINT and CMD. The process is actively executing on host CPU cores.
  3. Paused: All processes within the container are suspended in place. Docker uses the Linux cgroup freezer subsystem (cgroup.freeze in cgroups v2) to freeze process scheduling. The process table and memory state remain preserved in host RAM, but the process receives zero CPU cycles. Unlike a stopped container, a paused container does not execute shutdown hooks or terminate sockets.
  4. Stopped (Exited): The main container process (PID 1) has terminated, or was stopped by an operator. The process table entries are cleared from the host kernel, and CPU/RAM resources are freed. However, the container's configuration, metadata, and writable upperdir filesystem state remain intact on disk.
  5. Deleted (Removed): The container is deleted via docker rm. The runtime unmounts the OverlayFS filesystem, deletes the upperdir and workdir directories from /var/lib/docker/overlay2/, purges network endpoints, and deletes the container configuration metadata.

docker stop vs. docker kill: The PID 1 Problem

Understanding how containers stop is critical for zero-downtime deployments and data integrity. The difference between docker stop and docker kill centers on POSIX signals:

  • docker stop (Graceful Shutdown):
    1. Sends signal 15 (SIGTERM) to the root process inside the container's PID namespace (PID 1).
    2. Starts a configurable countdown timer (default: 10 seconds, configurable via -t or --time).
    3. Gives the application time to stop accepting new requests, drain active HTTP connections, finish in-flight database transactions, and flush write buffers.
    4. If the process has not terminated when the timeout expires, Docker sends signal 9 (SIGKILL), forcibly terminating the process immediately.
  • docker kill (Immediate Termination): Bypasses the grace period entirely and sends SIGKILL (or an operator-specified signal) directly to PID 1. The operating system kernel immediately halts process execution without allowing the application to clean up sockets or flush disk buffers.

The PID 1 Signal Swallowing Pitfall

Under standard Linux semantics, the process with PID 1 (traditionally init or systemd) has unique kernel privileges and responsibilities. Crucially, the Linux kernel does not apply default signal handlers to PID 1.

For an ordinary process (e.g., PID 450), receiving an unhandled SIGTERM causes the kernel to terminate it immediately. But if PID 1 has not explicitly registered a signal handler for SIGTERM, the kernel simply ignores the signal.

This creates the notorious "10-second shutdown stall" when containers are launched using the shell form of CMD or ENTRYPOINT:

# ❌ INCORRECT: Shell form spawns /bin/sh as PID 1
# /bin/sh does NOT forward SIGTERM to the node child process!
CMD node server.js

# ✅ CORRECT: Exec form executes node directly as PID 1
CMD ["node", "server.js"]

In the shell form, /bin/sh runs as PID 1, and the application runs as a child process (PID 2). When Docker sends SIGTERM, /bin/sh receives it, does not handle it, and does not forward it to node. The application continues running until Docker's 10-second timer expires, at which point Docker sends SIGKILL, abruptly crashing the service.

Deciphering Container Exit Codes

When a container transitions to the Exited state, its exit code reveals exactly how and why it terminated:

+-----------+-----------------------+-----------------------------------------------------------+
| Exit Code | Classification        | Common Root Causes                                        |
+-----------+-----------------------+-----------------------------------------------------------+
| 0         | Clean Exit            | Application finished successfully (batch job, task).      |
| 1 - 127   | Application Failure   | Uncaught runtime exceptions, missing config, syntax error.|
| 125       | Docker Daemon Error   | docker run failed (e.g., invalid flag, port conflict).    |
| 126       | Invocation Failure    | Command found but not executable (chmod +x missing).     |
| 127       | Command Not Found     | Binary does not exist in container PATH.                  |
| 137       | Fatal Signal 9        | SIGKILL: OOM Killer (cgroups memory.max) or docker kill.  |
| 143       | Fatal Signal 15       | SIGTERM: Graceful shutdown via docker stop.              |
+-----------+-----------------------+-----------------------------------------------------------+

POSIX standards define that when a process terminates due to an unhandled operating system signal NN, its exit status is calculated as:

Exit Code=128+N\text{Exit Code} = 128 + N

  • Exit Code 137 (128+9128 + 9): The process was terminated by signal 9 (SIGKILL). If an operator did not issue docker kill, this almost always indicates an Out-Of-Memory (OOM) kill. When a container exceeds its cgroup memory limit (--memory), the kernel's OOM killer terminates PID 1.
  • Exit Code 143 (128+15128 + 15): The process was terminated by signal 15 (SIGTERM). This indicates that docker stop requested termination, and the process halted in response.

5. Practical Hands-On Walkthrough: Inspecting Images and Containers

To solidify these concepts, let us execute concrete diagnostic commands that inspect the low-level OCI metadata of an image and observe the live OverlayFS mount of a running container.

Step 1: Building a Multi-Stage Image

Consider this self-contained, production-grade Dockerfile for a Node.js microservice:

# syntax=docker/dockerfile:1.4
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:22-bookworm-slim AS runner
WORKDIR /app
# Run as non-root user for security
USER node
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node index.js ./

EXPOSE 3000
CMD ["node", "index.js"]

Build the image:

docker build -t openstair/demo-service:1.0.0 .

Step 2: Inspecting Image Layers and Manifests

Every layer in the image corresponds to a read-only filesystem diff. Let us inspect the image's cryptographic structure:

# Inspect the uncompressed rootfs diff IDs vs the image ID
docker image inspect openstair/demo-service:1.0.0 --format '{{json .RootFS}}' | jq .

The output reveals the ordered cryptographic chain of layers:

{
  "Type": "layers",
  "Layers": [
    "sha256:4b91e5c4a5c898c88085d56561e1b1236fb6822c6fb586b6a2f3a63369d750c0",
    "sha256:72c8427f71b9d14695029a27bb1a5b809d43526c8868c267cf2432c2a3922d99",
    "sha256:2b24016147285153fb50949fa87fa76ec1966dbb7c02b28c8d8b671a8bc43d19"
  ]
}

Notice the critical distinction:

  • Manifest Layer Digest: The SHA-256 of the compressed tarball downloaded from the remote registry.
  • RootFS DiffID: The SHA-256 of the uncompressed filesystem layer extracted on the host machine.

Step 3: Running the Container and Inspecting OverlayFS

Launch a running container from the image:

docker run -d --name demo-app -p 3000:3000 openstair/demo-service:1.0.0

Now, inspect the host filesystem paths that make up the container's union mount:

docker inspect demo-app --format '{{json .GraphDriver.Data}}' | jq .

The Docker daemon prints the four OverlayFS paths configured in the host kernel:

{
  "LowerDir": "/var/lib/docker/overlay2/01ab.../diff:/var/lib/docker/overlay2/fe43.../diff",
  "MergedDir": "/var/lib/docker/overlay2/98cd.../merged",
  "UpperDir": "/var/lib/docker/overlay2/98cd.../diff",
  "WorkDir": "/var/lib/docker/overlay2/98cd.../work"
}
  • LowerDir contains the stacked read-only layers belonging to openstair/demo-service:1.0.0.
  • UpperDir is the isolated writable layer allocated exclusively to demo-app.
  • MergedDir is the active mount point that the container process sees as /.

Step 4: Proving Copy-on-Write and Whiteouts

We can verify the Copy-on-Write mechanism by modifying a file and inspecting what happens inside UpperDir.

First, write a new file inside the container:

docker exec -u 0 demo-app sh -c "echo 'hello world' > /app/runtime-note.txt"

Next, modify an existing file inherited from the image's base OS:

docker exec -u 0 demo-app sh -c "echo 'modified host' >> /etc/issue"

Finally, delete an inherited system file:

docker exec -u 0 demo-app sh -c "rm /etc/issue.net"

Now inspect the host path referenced by UpperDir (requires root privileges on the Linux host running Docker):

# Store the upperdir path in a shell variable
UPPER_DIR=$(docker inspect demo-app --format '{{.GraphDriver.Data.UpperDir}}')

# List the contents of UpperDir on the host
ls -la $UPPER_DIR/app/
# Shows: runtime-note.txt (The newly created file exists physically here)

ls -la $UPPER_DIR/etc/
# Shows: 
# 1. issue (Copied up from LowerDir and mutated)
# 2. issue.net (Character device whiteout with major/minor 0,0!)

If you check $UPPER_DIR/etc/issue.net, the file permissions show:

c--------- 1 root root 0, 0 Nov 12 10:00 issue.net

The c flag confirms that OverlayFS created a character device whiteout with major device 0 and minor device 0. The original /etc/issue.net in the image's lowerdir remains completely untouched.

Step 5: Implementing Graceful PID 1 Shutdown

To ensure the container responds to docker stop immediately rather than waiting out the 10-second timeout, the application code must intercept SIGTERM:

// index.js
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Service operational\n');
});

server.listen(3000, () => {
  console.log('HTTP server listening on port 3000');
});

// Intercept SIGTERM dispatched by docker stop
process.on('SIGTERM', () => {
  console.log('SIGTERM received: initiating graceful drainage...');
  
  // Stop accepting new connections
  server.close((err) => {
    if (err) {
      console.error('Error closing HTTP listener:', err);
      process.exit(1);
    }
    console.log('All connections drained. Exiting cleanly.');
    process.exit(0);
  });

  // Enforce a maximum shutdown deadline if clients fail to disconnect
  setTimeout(() => {
    console.error('Forced shutdown: connections did not drain in time.');
    process.exit(1);
  }, 8000);
});

Because our Dockerfile used the exec form (CMD ["node", "index.js"]), node runs as PID 1. When docker stop demo-app executes:

  1. Docker sends SIGTERM to PID 1.
  2. The process.on('SIGTERM') callback fires immediately.
  3. In-flight requests drain cleanly.
  4. The container exits with code 0 in under 100 milliseconds, bypassing the 10-second fallback kill.

6. Image vs. Container: The Definitive Comparison Matrix

The following matrix contrasts the architectural, operational, and lifecycle dimensions of Docker images and containers:

| Dimension | Docker Image | Docker Container | | :--- | :--- | :--- | | System Identity | Static binary artifact and configuration manifest. | Dynamic host operating system process execution. | | State & Mutability | Strictly Immutable: Cryptographically content-addressed by SHA-256 digest. Changes produce a new image. | Stateful & Mutable: Writes captured in ephemeral upperdir layer or external volumes. | | OCI Specification | Defined by the OCI Image Specification (manifest, config JSON, layer tarballs). | Defined by the OCI Runtime Specification (namespaces, cgroups, hooks, rootfs mounts). | | Host Resource Usage | Passive disk storage (layers on disk in /var/lib/docker/overlay2). Zero CPU or RAM consumption. | Active host process consuming physical RAM, CPU scheduling slices, and network bandwidth. | | Creation Mechanism | Built via docker build (BuildKit), docker commit (anti-pattern), or pulled from a registry. | Instantiated via docker create, docker run, or orchestration runtimes (containerd, CRI-O). | | Distribution Model | Pushed and pulled across remote OCI registries (Docker Hub, AWS ECR, GitHub Packages). | Local to the host engine. Cannot be "pushed" to a registry directly without committing to an image. | | Filesystem Architecture | Stacked read-only filesystem diffs (lowerdir). Multiple containers share identical layers. | Union mount: Read-only lowerdir layers fused with an ephemeral writable upperdir. | | Persistence Model | Permanent until manually pruned (docker image rm). | Ephemeral: Destroyed on docker rm. Persistent state must use Named Volumes or Bind Mounts. | | Execution Primitives | Tarballs, compressed gzip streams, and JSON documents. | Linux kernel namespaces (PID, NET, MNT, IPC, UTS, USER) and Control Groups (cgroups v2). |


7. Common Production Anti-Patterns and How to Avoid Them

Failing to understand the boundary between images and containers leads to four widespread production anti-patterns:

Anti-Pattern 1: Treating Containers as "Pets" (State in Upperdir)

  • The Mistake: Writing persistent user uploads, SQLite databases, or transaction logs directly to paths inside the container (e.g., /app/data/) without mounting a Docker Volume.
  • The Failure Mode: The moment the container crashes, is updated, or is recreated during a Kubernetes rolling update, the container is deleted (docker rm), and all data inside upperdir is permanently destroyed. Furthermore, writes incur the OverlayFS copy-up penalty.
  • The Solution: Always mount a Docker Named Volume (-v db-data:/var/lib/postgresql/data) for stateful workloads. Treat containers as disposable, stateless execution units.

Anti-Pattern 2: The docker commit Trap

  • The Mistake: Launching a base container, attaching via docker exec, installing packages manually via apt-get, configuring configuration files by hand, and saving the result using docker commit.
  • The Failure Mode: The resulting image lacks provenance. No one knows which exact package versions were installed, build steps cannot be reproduced in CI/CD, security patches cannot be applied systematically, and the image accumulates massive layer bloat from intermediate caches.
  • The Solution: Maintain 100% of image definitions in version-controlled Dockerfiles. Automated CI/CD pipelines should build images deterministically from source repositories.

Anti-Pattern 3: Ignoring Multi-Architecture Manifest Resolution

  • The Mistake: A developer builds an image on an Apple Silicon (ARM64) workstation using docker build -t my-app:latest . and pushes it to a registry. Production runs on x86_64 (AMD64) Linux cloud instances.
  • The Failure Mode: The production container crashes immediately on launch with:
    exec /usr/local/bin/app: exec format error
    
    The host kernel cannot execute ARM64 machine instructions on an AMD64 CPU.
  • The Solution: Use Docker Buildx to cross-compile and publish multi-platform OCI Image Indexes:
    docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.com/app:1.0.0 --push .
    

Anti-Pattern 4: The Shell-Form Process Invocation

  • The Mistake: Using CMD npm start or ENTRYPOINT ./start.sh in the Dockerfile.
  • The Failure Mode: /bin/sh runs as PID 1 and swallows SIGTERM. When deployments occur, Kubernetes or Docker waits 10 seconds before forcefully killing the container with SIGKILL. Active database connections drop abruptly, web clients receive 502 Bad Gateway errors, and deployments stall.
  • The Solution: Always use the JSON array exec syntax: CMD ["npm", "start"]. For scripts requiring setup, ensure the shell script finishes with exec "$@" so that the final application replaces the shell and assumes PID 1 directly.

8. Production Verification Checklist

Before deploying containerized services into staging or production, run through this verification checklist to validate image hygiene and container runtime boundaries:

IMAGE IMMUTABILITY & HYGIENE
[ ] The image is built deterministically via automated CI/CD from a versioned Dockerfile.
[ ] Base images use pinned, immutable tags or SHA-256 digests rather than mutable :latest.
[ ] Multi-stage builds separate compile-time toolchains (compilers, SDKs) from the minimal runtime image.
[ ] Multi-architecture builds publish an OCI Image Index supporting target deployment architectures.
[ ] Vulnerability scans (e.g., Trivy, Grype) run during CI to catch vulnerable system libraries.

CONTAINER RUNTIME BOUNDARIES
[ ] The container runs as an unprivileged non-root user (USER nonroot).
[ ] Control groups enforce strict memory limits (--memory) and CPU quotas (--cpus) to prevent host exhaustion.
[ ] The process execution uses the exec syntax (CMD ["binary", "arg"]) so the app receives signals as PID 1.
[ ] The application registers explicit SIGTERM handlers to drain active connections within the grace window.
[ ] The container healthcheck (HEALTHCHECK) verifies actual service readiness, not just process presence.

STORAGE & PERSISTENCE TOPOLOGY
[ ] No persistent application data or high-throughput logs write to the ephemeral OverlayFS upperdir.
[ ] Databases and persistent caches use dedicated Docker Named Volumes or host-managed cloud storage.
[ ] High-frequency scratchpads or sensitive decryption keys use in-memory tmpfs mounts.
[ ] The root filesystem is mounted read-only (--read-only) where possible, with writable tmpfs on /tmp.

Conclusion

Understanding the difference between Docker images and containers is the dividing line between treating containers as fragile virtual machines and operating them as predictable, cloud-native systems.

An image is a static, immutable, cryptographically verifiable artifact—a serialized OCI bundle of configuration metadata and layered filesystem tarballs designed for deterministic sharing across registries.

A container is a running or stopped instance of that image—a standard Linux host process governed by kernel namespaces, restricted by cgroups quotas, and executing on top of an ephemeral OverlayFS union mount.

When you draw clean boundaries between immutable image layers, isolated process execution, and independent volume persistence, containerized applications become faster to build, resilient against crashes, and safe to deploy at scale.

Related articles

Illustration of a software delivery pipeline from commit through tests and deployment
DevOps

Zero-Downtime Deployment: What It Really Requires

November 19, 2025 · 16 min read

A deep dive into zero-downtime deployments: endpoint de-registration races, graceful shutdown, rolling vs blue-green vs canary topologies, and expand-contract database migrations.

DevOpsCI/CDCloudSoftware Engineering