Docker
Containers, images, volumes, multi-stage builds.
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.