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.

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:
- 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. - 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). - 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. - 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 asgolang:1.24-alpine) to multiple distinct platform manifests (such aslinux/amd64,linux/arm64/v8, orlinux/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:
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 hostlowerdirpaths in memory and on disk.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.workdir(Internal Scratchpad): An auxiliary directory required by the Linux kernel OverlayFS implementation. It must reside on the same host filesystem asupperdir. OverlayFS usesworkdirto stage files during copy-up operations before atomically moving them intoupperdir, preventing partial file corruptions if a write is interrupted.merged(Unified Mountpoint): The directory where the kernel exposes the union view. This is the mount target passed intopivot_rootorchrootduring container initialization. From inside the container, processes seemergedas 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
upperdirfirst. 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 thelowerdirstack, 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
lowerdirlayer up intoupperdir. - Once the copy completes, the process performs its write operation on the newly created replica inside
upperdir. The original file inlowerdirremains pristine and unchanged. - Subsequent writes to this file occur directly within
upperdirwithout 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
lowerdirtoupperdirbefore 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 inupperdirwith major and minor device numbers set to0,0(c 0 0), named identically to the deleted file. - When the kernel scans the filesystem, encountering this
0,0character device inupperdircauses it to mask and hide the corresponding file inlowerdir. The file appears deleted to all container processes. - Similarly, if an entire directory inherited from
lowerdiris removed, OverlayFS creates an opaque directory inupperdirmarked with the extended attributetrusted.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:
- Directory Lookup Traversal:
Every time the kernel resolves a file path across an OverlayFS mount, it must traverse multiple stacked
lowerdirlayers. As the number of image layers increases, directory cache (dentry) lookups incur additional CPU overhead compared to a flat ext4 or XFS filesystem. - First-Write Copy-Up Overhead:
As detailed above, modifying any file bundled into the image triggers a synchronous byte-for-byte copy to
upperdir. - Page Cache Duplication:
When an unmodified file in
lowerdiris read, its data resides in the Linux kernel's page cache. Once that file is copied up toupperdirand 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 |
+-------------------+
- Created:
The container object has been instantiated. Docker has assigned a unique 64-character hexadecimal container ID, created the dedicated
upperdirandworkdirstorage paths on the host, parsed environment variables, and configured network settings. However, no host process has been spawned. - Running:
The container runtime (
runc) has invokedclone()with isolated namespace flags, mounted the rootfs via OverlayFS, applied cgroups limits, and executed the image'sENTRYPOINTandCMD. The process is actively executing on host CPU cores. - Paused:
All processes within the container are suspended in place. Docker uses the Linux cgroup freezer subsystem (
cgroup.freezein 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. - 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
upperdirfilesystem state remain intact on disk. - Deleted (Removed):
The container is deleted via
docker rm. The runtime unmounts the OverlayFS filesystem, deletes theupperdirandworkdirdirectories 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):- Sends signal 15 (
SIGTERM) to the root process inside the container's PID namespace (PID 1). - Starts a configurable countdown timer (default: 10 seconds, configurable via
-tor--time). - Gives the application time to stop accepting new requests, drain active HTTP connections, finish in-flight database transactions, and flush write buffers.
- If the process has not terminated when the timeout expires, Docker sends signal 9 (
SIGKILL), forcibly terminating the process immediately.
- Sends signal 15 (
docker kill(Immediate Termination): Bypasses the grace period entirely and sendsSIGKILL(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 , its exit status is calculated as:
- Exit Code 137 ():
The process was terminated by signal 9 (
SIGKILL). If an operator did not issuedocker 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 ():
The process was terminated by signal 15 (
SIGTERM). This indicates thatdocker stoprequested 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"
}
LowerDircontains the stacked read-only layers belonging toopenstair/demo-service:1.0.0.UpperDiris the isolated writable layer allocated exclusively todemo-app.MergedDiris 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:
- Docker sends
SIGTERMto PID 1. - The
process.on('SIGTERM')callback fires immediately. - In-flight requests drain cleanly.
- The container exits with code
0in 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 insideupperdiris 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 viaapt-get, configuring configuration files by hand, and saving the result usingdocker 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:
The host kernel cannot execute ARM64 machine instructions on an AMD64 CPU.exec /usr/local/bin/app: exec format error - 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 startorENTRYPOINT ./start.shin the Dockerfile. - The Failure Mode:
/bin/shruns as PID 1 and swallowsSIGTERM. When deployments occur, Kubernetes or Docker waits 10 seconds before forcefully killing the container withSIGKILL. Active database connections drop abruptly, web clients receive502 Bad Gatewayerrors, and deployments stall. - The Solution: Always use the JSON array exec syntax:
CMD ["npm", "start"]. For scripts requiring setup, ensure the shell script finishes withexec "$@"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.


