🌙
☀️ Dark
PART 27

Docker

Containers, images, volumes, multi-stage builds.

Intermediate 45 min read

Volume 27: Docker & Containers

Learning Objectives

  • Understand the fundamental difference between VMs and Containers.
  • Build, run, and manage Docker Images and Containers.
  • Understand Docker layers, networking, and volumes.
  • Orchestrate multi-container applications using Docker Compose.
  • Optimize builds using multi-stage Dockerfiles.
  • Implement production-ready container security and health checks.

Prerequisites

Basic understanding of Linux file systems and processes. Familiarity with Node.js or a backend language. Basic terminal proficiency.

Why Does This Exist?

"It works on my machine." This phrase is the bane of software engineering. Docker exists to completely eliminate it by packaging the application and its entire environment into a single, portable unit.

The Problem Before the Solution

Before containers, deploying an application meant configuring a server manually or using configuration management tools like Ansible/Chef. You had to install specific versions of Node.js, Python, PostgreSQL, and system libraries. If another app on the same server needed a different version of a library, you had dependency hell. We solved this with Virtual Machines (VMs).

Why the Old Approach Breaks

VMs solve isolation but introduce massive overhead. Every VM runs a full guest Operating System (OS). If you have three apps in three VMs, you are running three copies of Linux, taking up gigabytes of RAM and CPU cycles just for the OS. Boot times are measured in minutes.

History

Linux introduced chroot in 1979 to isolate file systems. Later, cgroups (control groups) and namespaces were added to Linux to limit and isolate resource usage (CPU, memory, networking) per process. In 2013, dotCloud (later Docker Inc.) combined these low-level kernel features with an easy-to-use CLI and image format, revolutionizing software delivery.

Mental Model (Analogy -> Reality)

Analogy: Think of a shipping port before standard shipping containers. Goods were packed in barrels, sacks, and boxes. Loading a ship was complex and custom. Shipping containers standardized the process: the crane doesn't care what's inside the container, it just knows how to move a standard box.

Reality: Docker containers standardize software. The server doesn't care if it's a Node app, a Python script, or a database. It just runs the container. Inside, it's just an isolated Linux process running on the host OS.

Internal Working

Containers are NOT lightweight VMs. A container is just a normal Linux process. However, Docker uses two kernel features:

  • Namespaces: What the process can see. It gives the process its own isolated filesystem, process tree, network stack, etc. It thinks it's the only process on the machine.
  • cgroups: What the process can use. It limits how much CPU and RAM the process can consume.

Visual Explanation

[ Virtual Machines ]           [ Containers (Docker) ]
+------------------+           +------------------+
|      App A       |           | App A |  App B   |
|   Bins/Libs A    |           | Bins  |  Bins    |
| Guest OS (Linux) |           +------------------+
+------------------+           |  Docker Engine   |
|    Hypervisor    |           +------------------+
+------------------+           |   Host OS (Linux)|
|   Hardware       |           +------------------+
|                  |           |     Hardware     |
+------------------+           +------------------+
  

Syntax

# Dockerfile
FROM node:18-alpine     # Base image
WORKDIR /app            # Set working directory
COPY package.json .     # Copy dependency file
RUN npm install         # Run command during build
COPY . .                # Copy source code
CMD ["node", "app.js"]  # Default command to run
  

Tiny Example

Let's run a simple container.

docker run -d -p 8080:80 nginx
  

This pulls the nginx image, runs it in the background (-d), and maps port 8080 on your host to port 80 inside the container.

Walkthrough

1. We write a Dockerfile.
2. We build the image: docker build -t my-app .
3. The image is composed of Layers. Each command in the Dockerfile creates a new read-only layer.
4. We run the container: docker run my-app. Docker adds a thin read-write layer on top of the image.
5. The container processes start running in their isolated namespace.

Break It

What happens if we write data inside a container, and then the container stops?

docker run -it ubuntu bash
root@...:/# touch /my_important_file.txt
exit
  

If we start a new Ubuntu container, my_important_file.txt is gone. Container filesystems are ephemeral.

Debug It

To persist data, we use Volumes. Volumes exist outside the container's union filesystem.

docker run -v my_data:/app_data ubuntu bash
  

Now, data written to /app_data survives container restarts.

Mini Project

Dockerizing a Node API with Redis using Docker Compose.

# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports: ["3000:3000"]
    environment:
      - REDIS_URL=redis://redis:6379
  redis:
    image: redis:alpine
  

Here, Docker Compose automatically creates a Network allowing `api` to talk to `redis` via DNS.

Real Application Feature

Multi-stage builds. In real apps, we need a build step (TypeScript -> JS), but we don't want the TS compiler in our final production image to save space and reduce attack surface.

# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package.json .
RUN npm install --production
CMD ["node", "dist/app.js"]
  

Production Implementation

Production containers need restrictions. Never run as root.

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
  

And add Health Checks:

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/health || exit 1
  

Production Usage

In production, you rarely run raw docker run commands. You push your images to a registry (Docker Hub, AWS ECR), and an orchestrator like Kubernetes or AWS ECS pulls the images and manages the containers across multiple servers.

Performance

Optimize layer caching. Put commands that change least frequently (like installing dependencies) at the top of the Dockerfile, and source code COPY at the bottom.

Best Practices

  • One process per container.
  • Use small base images (e.g., alpine or distroless).
  • Use .dockerignore to exclude node_modules and secrets.
  • Pass configuration via Environment Variables (12-Factor App).

Interview Questions

Easy: What is the difference between a container and an image?

An image is a read-only template containing the app and its environment. A container is a running instance of an image.

Medium: How do containers communicate with each other?

Via Docker Networks. Containers on the same user-defined bridge network can communicate using their container names as hostnames.

Hard: How does layer caching work in Docker?

Docker steps through the Dockerfile. If a command and its inputs haven't changed, it reuses the cached layer. If a layer changes, all subsequent layers must be rebuilt.

Senior: Explain how cgroups and namespaces enable containers.

Namespaces provide isolation (PID, NET, MNT, IPC, UTS) making the process think it's isolated. Cgroups enforce limits on resources like CPU, Memory, and I/O so one container can't starve others.

Engineering Challenge

Create a Docker Compose setup with a Node backend, React frontend, and Postgres database. Make sure the database uses a named volume, and the frontend only hot-reloads via a bind mount in development.

View Solution

Use volumes: [".:/app", "/app/node_modules"] for the frontend to bind mount code while keeping node_modules internal. Use a named volume postgres_data:/var/lib/postgresql/data for the DB.

Revision Sheet

Commands: build, run, ps, stop, rm, rmi, exec, logs.
Concepts: Layers (immutable filesystem), Volumes (persistent data outside union FS), Networks (DNS isolation).
Optimization: Multi-stage builds, layer caching, non-root user.

Connections

Containers are the fundamental building block for CI/CD pipelines (Vol 28) and Kubernetes Orchestration (Vol 29). The network namespaces inside Docker also mirror reverse proxy concepts from Volume 15.

🏠 Curriculum NextVolume 2

Mini Project (20-30 min)

▶ View Solution

Dockerizing a Node API with Redis using Docker Compose.

# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports: ["3000:3000"]
    environment:
      - REDIS_URL=redis://redis:6379
  redis:
    image: redis:alpine
  

Here, Docker Compose automatically creates a Network allowing `api` to talk to `redis` via DNS.

Bigger Project (1-2 hours)

Apply all concepts from this volume to build a comprehensive feature.

▶ View Solution
typescript
// Example project code here

Interview Questions

Easy: What is the difference between a container and an image?

An image is a read-only template containing the app and its environment. A container is a running instance of an image.

Medium: How do containers communicate with each other?

Via Docker Networks. Containers on the same user-defined bridge network can communicate using their container names as hostnames.

Hard: How does layer caching work in Docker?

Docker steps through the Dockerfile. If a command and its inputs haven't changed, it reuses the cached layer. If a layer changes, all subsequent layers must be rebuilt.

Senior: Explain how cgroups and namespaces enable containers.

Namespaces provide isolation (PID, NET, MNT, IPC, UTS) making the process think it's isolated. Cgroups enforce limits on resources like CPU, Memory, and I/O so one container can't starve others.