Introduction to Docker and Containerization

SA
StudyAI
AI-generated study notes
· Published Updated Pro Plus model

From the Make notes on docker an GitHub curriculum

TL;DR

Docker packages an application and everything it needs — code, libraries, system tools — into a single portable unit called a container. Containers run consistently on any machine with Docker installed, ending the "it works on my machine" problem. You'll learn how containers differ from virtual machines, how Docker's architecture fits together, and how to build your own image with a Dockerfile.

1. The Mental Model

Think of a container as a sealed lunchbox: everything your app needs to run is packed inside it, so it tastes the same wherever you open it. A virtual machine is more like shipping the entire kitchen — stove, fridge, and all — just to make one sandwich. Docker gives you the lunchbox approach: lightweight, fast to start, and identical everywhere. A container is your app plus its dependencies, isolated from the host but sharing its kernel.

2. The Core Material

2.1 Containers vs Virtual Machines

Large cargo ship docked at Terminal Burchardkai in Hamburg with containers and cranes.
Photo by Anna Hinckel on Pexels

A virtual machine (VM) virtualizes hardware. Each VM runs a full guest operating system on top of a hypervisor, which sits on the host OS. That means every VM carries its own kernel, its own copy of system libraries, and boots in tens of seconds to minutes. A typical VM image is gigabytes in size.

A container virtualizes the operating system, not the hardware. Containers share the host machine's kernel but isolate the application's filesystem, processes, and network using Linux kernel features called namespaces (isolation) and cgroups (resource limits — CPU, memory). Because there's no second kernel to boot, containers start in milliseconds and images are typically tens to hundreds of megabytes.

Virtual Machine Container
Isolation unit Full OS (own kernel) Process (shared host kernel)
Boot time Seconds to minutes Milliseconds
Typical size GBs MBs
Density per host Few dozen Hundreds
Isolation strength Very strong (hardware-level) Good, but weaker than a VM

This is why teams increasingly run containers inside VMs in the cloud: the VM gives strong security isolation between tenants, and containers inside it give fast, lightweight app packaging.

2.2 Docker Architecture: Client, Daemon, Images, Registries

Red container ship docked at Hamburg Terminal under cranes on a cloudy day.
Photo by Wolfgang Weiser on Pexels

Docker uses a client-server model with four main pieces:

  • Docker Client (docker CLI): the tool you type commands into (docker run, docker build).
  • Docker Daemon (dockerd): a background process that does the actual work — building images, running containers, managing networks and volumes. It listens for API requests from the client.
  • Images: read-only templates built from a set of instructions (a Dockerfile). An image is like a class in object-oriented programming.
  • Containers: a running (or stopped) instance of an image — like an object instantiated from that class. You can run many containers from one image.
  • Registry (e.g. Docker Hub, GitHub Container Registry): a remote store for images, where you push images to share them and pull them to run elsewhere.
flowchart LR
    Dev["Developer runs: docker build"] --> Client["Docker Client (CLI)"]
    Client -->|REST API call| Daemon["Docker Daemon (dockerd)"]
    Daemon -->|creates| Image["Image (read-only layers)"]
    Image -->|docker run| Container["Running Container"]
    Daemon -->|docker push| Registry["Registry (Docker Hub / GHCR)"]
    Registry -->|docker pull| Daemon

When you run docker run nginx, the daemon checks if the nginx image exists locally. If not, it pulls it from the registry, then creates a writable container layer on top of the image's read-only layers, and starts a process inside an isolated namespace.

2.3 Building Images with a Dockerfile and Layers

Urban building in Vietnam with reflective glass showcasing layered architecture and city vibes.
Photo by Kunno Jayson on Pexels

A Dockerfile is a text file of instructions that Docker reads top-to-bottom to build an image. Each instruction creates a new layer — a diffable, cacheable filesystem snapshot. Layers are stacked and shared across images, which is why Docker builds are fast after the first run and why images with common base layers (like node:18) don't duplicate storage.

Key instructions:
- FROM — sets the base image (must be the first instruction)
- WORKDIR — sets the working directory inside the container
- COPY — copies files from your machine into the image
- RUN — executes a command at build time (e.g. installing dependencies), creating a new layer
- EXPOSE — documents which port the container listens on (doesn't actually publish it)
- CMD — the default command run when the container starts

Layer caching matters: Docker reuses a cached layer if the instruction and its context haven't changed. This is why you should COPY package.json and run npm install before copying the rest of your source code — your dependencies rarely change, but your code does, so you avoid re-installing packages on every code edit.

3. Worked Example

Let's containerize a tiny Node.js web server, from source code to a running container.

Step 1 — the app (index.js):

const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from inside a container!\n');
});
server.listen(3000, () => console.log('Server running on port 3000'));

Step 2 — the Dockerfile:

# Base image with Node.js pre-installed
FROM node:18-alpine

# Set working directory inside the container
WORKDIR /app

# Copy only dependency files first (layer caching)
COPY package*.json ./
RUN npm install --omit=dev

# Now copy the rest of the source code
COPY . .

# Document the port the app listens on
EXPOSE 3000

# Default command when container starts
CMD ["node", "index.js"]

Step 3 — build the image:

docker build -t hello-container:1.0 .

Expected output ends with something like:

=> exporting to image
=> => naming to docker.io/library/hello-container:1.0

Step 4 — run the container, mapping host port 8080 to container port 3000:

docker run -d -p 8080:3000 --name my-app hello-container:1.0

-d runs it detached (in the background), -p publishes the port, --name gives it a friendly name.

Step 5 — verify:

curl http://localhost:8080
# Hello from inside a container!
docker ps
# shows my-app as Up
docker logs my-app
# Server running on port 3000

Step 6 — push to a registry so others (or GitHub Actions) can pull it:

docker tag hello-container:1.0 ghcr.io/yourname/hello-container:1.0
docker push ghcr.io/yourname/hello-container:1.0

Notice the whole lifecycle: write code → build image (layers created, cached on rebuild) → run container (isolated process, mapped port) → push to registry (shareable anywhere Docker runs).

4. Production Pitfalls & Best Practices

4.1 Real-World Best Practices

Three women practicing yoga indoors at a studio in Rishikesh, India.
Photo by Yoga Vidya Mandiram on Pexels

  • Use small base images. Prefer node:18-alpine or python:3.12-slim over full OS images — smaller attack surface and faster pulls.
  • Order Dockerfile instructions by change frequency. Put rarely-changing steps (installing dependencies) before frequently-changing ones (copying source) to maximize cache hits.
  • Use multi-stage builds to keep final images lean — compile/build in one stage, copy only the output into a minimal runtime stage.
  • Never bake secrets into images. Use environment variables or Docker secrets at runtime, never COPY .env or hardcoded API keys in a Dockerfile.
  • Pin versions. Use node:18.19.0-alpine instead of node:latest so builds are reproducible months later.
  • Add a .dockerignore file (like .gitignore) to exclude node_modules, .git, and build artifacts from the build context — speeds up builds and avoids leaking files.
  • Run as a non-root user inside the container for security, using USER in the Dockerfile.

4.2 Common Bugs & Anti-Patterns

1. Copying everything before installing dependencies (breaks caching):

# BAD — any code change invalidates the npm install cache
COPY . .
RUN npm install
# FIXED — dependency layer only rebuilds when package.json changes
COPY package*.json ./
RUN npm install
COPY . .

2. Using latest tag in production:

# BAD — unpredictable, could break silently on rebuild
FROM node:latest
# FIXED — reproducible build
FROM node:18.19.0-alpine

3. Forgetting to expose/publish the port, then wondering why curl fails:

# BAD — container runs but nothing is reachable from the host
docker run -d hello-container:1.0
# FIXED — explicitly map host:container ports
docker run -d -p 8080:3000 hello-container:1.0

4. Giant images from unnecessary build tools:

# BAD — ships compilers and build tools in the final image
FROM node:18
RUN npm install && npm run build
CMD ["node", "dist/server.js"]
# FIXED — multi-stage build, final image has no build tools
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

4.3 When to Use What (Decision Matrix)

Situation Recommended Approach
Need strong isolation between untrusted tenants Virtual Machine (or container + VM combo, e.g. Firecracker)
Packaging a single app for consistent deployment Docker container
Compiled language needing build tools only at build time Multi-stage Dockerfile
Sharing images across a team or with CI/CD Push to a registry (Docker Hub, GHCR, ECR)
Local development matching production exactly docker-compose with the same images used in prod
App needs persistent data across container restarts Docker volumes, not the container's writable layer

5. Now Try It

Right here, without leaving this page: write out the full Dockerfile for a Python Flask app with this structure — app.py (your code), requirements.txt (dependencies), base image python:3.12-slim, dependencies installed before code is copied, app listening on port 5000, and running as a non-root user (hint: use RUN adduser and USER). Then write the three CLI commands you'd run in order to build the image as flask-demo:1.0, run it mapping host port 9000 to container port 5000, and check its logs.

Success looks like: you have a complete, correctly-ordered Dockerfile (dependencies before code copy, non-root USER line included) plus the three correct docker build, docker run -p, and docker logs commands, without needing to check documentation.

Frequently asked about Introduction to Docker and Containerization

Docker packages an application and everything it needs — code, libraries, system tools — into a single portable unit called a container. Containers run consistently on any machine with Docker installed, ending the "it works on my machine" problem. Read the full notes above for the details.

Introduction to Docker and Containerization is a core topic in Make notes on docker an GitHub. Most exam papers test it via a mix of definitions, worked examples, and applied problems. The notes above cover the high-yield sub-topics, common pitfalls, and the kind of questions examiners typically set.

Yes — every note in the StudyAI Campus Hub is free to read in full, right here on this page, with no account needed. If you clone the plan into your own dashboard, the free plan shows a preview of each note there; Basic and above unlock the full notes in your dashboard, along with practice quizzes, flashcards and offline study. You can always come back here to read the complete note for free.
Continue with
Building Docker Images and Dockerfiles

More from Make notes on docker an GitHub


Get the full Make notes on docker an GitHub curriculum

Clone the complete plan to your dashboard for unlimited AI-generated notes, practice quizzes, and a personalised revision schedule.

Save this course free