quyennv.com

Senior DevOps Engineer · Healthcare, Singapore

Docker: Containers, Images, and the Basics

#docker#containerization#virtualization#containers#devops

Docker is a platform for building, shipping, and running applications in containers. Containers package your app and its dependencies into a single runnable unit that behaves the same everywhere: on your laptop, in CI/CD, or in the cloud.

Why containers?

  • Consistency: Same image runs the same way in dev, test, and production.
  • Isolation: Each container has its own filesystem and network namespace.
  • Portability: Build once, run on any host that supports Docker (or another container runtime).
  • Efficiency: Containers share the host kernel and start quickly compared to virtual machines.

Docker architecture

Docker uses a client–server model. When you run docker run or docker build, the Docker client (CLI) talks to the Docker daemon (dockerd), which does the real work.

+------------------+   REST API (e.g. over socket)   +------------------------+
|  Docker Client   | -----------------------------> |    Docker Daemon        |
|  (docker CLI)    |                                |    (dockerd)            |
+------------------+                                +------------+------------+
                                                                 |
    +------------------------------------------------------------+-----------------------------+
    |                                                            v                             |
    |  +-------------+  +-------------+  +-----------------+  +---------------------+         |
    |  |  Images &   |  | containerd  |  |      runc       |  |  Network, volumes,  |         |
    |  |  layer      |  | (runtime    |->|  (OCI runtime)  |  |  build, etc.        |         |
    |  |  storage    |  |  lifecycle) |  |  creates actual |  |                     |         |
    |  |             |  |             |  |  container     |  |                     |         |
    |  +-------------+  +-------------+  +-----------------+  +---------------------+         |
    +---------------------------------------------------------------------------------------------+
                                         Host
  • Docker daemon (dockerd): Builds images, manages containers, images, networks, and volumes. Listens for requests from the client.
  • containerd: Container runtime that manages the lifecycle of containers (start, stop, delete). Docker delegates container execution to containerd.
  • runc: Low-level OCI-compliant runtime that creates and runs the actual process in a constrained environment (namespaces, cgroups). containerd calls runc to create the container process.
  • Image and layer storage: Images and their layers are stored on disk (e.g. under /var/lib/docker on Linux). The daemon uses a storage driver (e.g. overlay2) to assemble layers into a container filesystem.

So the flow for docker run is: Client → dockerd → containerd → runc → your process runs inside the container.

Docker image layers

An image is not a single file; it is made of read-only layers stacked on top of each other. Each Dockerfile instruction that changes the filesystem (e.g. RUN, COPY, ADD) typically creates a new layer.

How layers work

  • Read-only: Every layer is immutable. Once built, it is reused by any image or container that needs it.
  • Union mount (e.g. OverlayFS): When you run a container, the storage driver merges all image layers into a single view. The container also gets a thin writable layer on top for any changes at runtime (e.g. creating files). That writable layer is discarded when the container is removed unless you use a volume.
  • Layering and caching: If you rebuild an image, Docker reuses cached layers until an instruction changes. Then that instruction and everything after it are rebuilt.

Example: this Dockerfile produces several layers:

FROM node:20-alpine    # Layer 1: base image (many layers from node:20-alpine)
WORKDIR /app          # Layer 2: create /app (metadata + empty dir in layer)
COPY package*.json ./ # Layer 3: add files
RUN npm ci             # Layer 4: install deps (new files in this layer)
COPY . .               # Layer 5: add app code
CMD ["node", "app.js"] # Layer 6: metadata only (no new filesystem layer)

Only instructions that change the filesystem add a new filesystem layer; WORKDIR, ENV, CMD, EXPOSE etc. often add only metadata.

Why layer order matters

  • Cache reuse: Put instructions that change less often (e.g. COPY package*.json + RUN npm ci) before instructions that change often (e.g. COPY . .). That way code changes do not invalidate the dependency-install layer.
  • Smaller images: Fewer or smaller layers mean less to pull and store. Combine related RUN commands to avoid extra layers (e.g. RUN apt update && apt install -y ... in one line).

Inspecting layers

# Show the layers (history) of an image
docker history my-app:latest

# Inspect image details, including layer IDs and config
docker image inspect my-app:latest

Summary: image layers

IdeaDetail
LayerOne read-only filesystem delta (or metadata). Many layers form one image.
Union filesystemLayers are stacked; the container sees one merged filesystem plus a writable top layer.
CacheRebuild reuses layers until an instruction changes; then that layer and all below are rebuilt.
Best practiceOrder Dockerfile so rarely changing steps come first; combine RUN steps to reduce layers.

Core concepts

TermMeaning
ImageRead-only template: OS layer + app + dependencies. Built from a Dockerfile or pulled from a registry.
ContainerA running instance of an image. You can run many containers from the same image.
DockerfileText file with instructions to build an image (e.g. FROM, RUN, COPY, CMD).
RegistryStorage for images (Docker Hub, Azure ACR, AWS ECR, etc.).

Dockerfile example

Each instruction that modifies the filesystem adds a layer (see Docker image layers above). This example keeps dependency install in its own layer so that layer can be cached when only app code changes:

# Use an official runtime as base
FROM node:20-alpine

# Set working directory inside the container
WORKDIR /app

# Copy dependency list and install
COPY package*.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Expose port and define default command
EXPOSE 3000
CMD ["node", "server.js"]

Build the image:

docker build -t my-app:latest .

Running containers

# Run in foreground (logs in terminal)
docker run -p 8080:3000 my-app:latest

# Run in background (detached)
docker run -d --name web -p 8080:3000 my-app:latest

# Run with environment variable
docker run -e NODE_ENV=production -p 8080:3000 my-app:latest

# Remove container when it stops
docker run --rm -p 8080:3000 my-app:latest

Essential commands

# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List images
docker images

# Stop a container
docker stop web

# Remove a container
docker rm web

# Remove an image
docker rmi my-app:latest

# View logs
docker logs web

# Execute a command inside a running container
docker exec -it web sh

Docker Compose (multi-container)

For apps that need several services (app + database + cache), use a docker-compose.yml:

version: "3.9"
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Run everything:

docker compose up -d

Summary

  • Images are built from Dockerfiles (or pulled); containers are running instances of images.
  • Use docker build and docker run for single services; use Docker Compose for multi-service setups.
  • Combine with CI/CD and orchestration (e.g. Kubernetes) to run containers at scale.

← All posts

Comments