Docker

Spread the love
Containerization & Docker β€” A Learning Resource
Containerization  Β·  Beginner β†’ Intermediate

Learn Docker from the ground up.

Why containers exist, how they differ from virtual machines, and every command and pattern you need to ship software that runs the same everywhere.

Virtual Machines
App A
App B
App C
Bins/Libs
Bins/Libs
Bins/Libs
Guest OS
Guest OS
Guest OS
Hypervisor
Host Operating System
Infrastructure
Each app ships a full OS Β· GBs Β· slow boot
Containers
App A
App B
App C
Bins/Libs
Bins/Libs
Bins/Libs
Docker Engine
Host Operating System
Infrastructure
Shared kernel Β· MBs Β· starts in seconds
14 chapters 60+ commands Copy-ready examples No prior containers needed
Jump to a chapter
01 β€” The problem

Why Docker exists

Almost every developer has said the words β€œbut it works on my machine.” Docker is the tool that made that sentence obsolete.

Software rarely runs alone. A single application depends on a specific language runtime, a set of libraries at exact versions, environment variables, a particular operating system, and often other services like a database or cache. When any one of those differs between your laptop, your teammate’s laptop, the test server, and production, things break in ways that are painful to reproduce.

Before containers, teams fought this with long setup docs, manual configuration, and virtual machines that were heavy and slow. Docker’s answer is simple: package the application together with everything it needs to run into a single, portable, self-sufficient unit called a container. That unit behaves identically whether it runs on your machine, a colleague’s, a CI server, or a cloud host.

The concrete pain points it removes

β€œWorks on my machine”
The app and its exact dependencies travel together, so the runtime environment is identical everywhere.
Dependency hell
Two projects needing different versions of the same library no longer conflict β€” each lives in its own isolated container.
Slow onboarding
A new hire runs one command instead of following a 40-step README to configure their machine.
Environment drift
Dev, staging, and production are built from the same image, so they can’t silently diverge over time.
Wasteful VMs
Containers share the host kernel, so you run many more of them on the same hardware, starting in seconds not minutes.
Hard to scale
A container is a repeatable unit, so scaling out becomes β€œrun more copies” β€” the foundation for microservices and orchestration.
In one sentence Docker lets you build an application into a portable image, ship that image anywhere, and run it identically β€” build once, run anywhere.
02 β€” The distinction

Virtualization vs. containerization

To understand what makes containers special, it helps to understand what came before them: virtual machines.

What is virtualization?

Virtualization lets one physical computer act like several. A layer of software called a hypervisor carves the physical hardware into multiple virtual machines (VMs), each of which runs its own complete guest operating system, with its own virtual CPU, memory, and disk.

  • Type 1 (bare-metal) hypervisors run directly on the hardware β€” e.g. VMware ESXi, Microsoft Hy-V, KVM. Common in data centers.
  • Type 2 (hosted) hypervisors run as an app on top of a normal OS β€” e.g. VirtualBox, VMware Workstation. Common on laptops.

VMs give you strong isolation and let you run entirely different operating systems on one machine. The cost is weight: every VM ships a full guest OS, which means gigabytes of disk, significant RAM, and boot times measured in minutes.

What is containerization?

A container also isolates an application, but instead of virtualizing hardware and booting a whole OS, it virtualizes the operating system. All containers on a host share that host’s kernel, while remaining isolated from each other. What’s inside a container is just your app plus its libraries and dependencies β€” not a second copy of the OS.

The result is dramatically lighter units: megabytes instead of gigabytes, starting in milliseconds to seconds, so a single host can run dozens or hundreds of containers where it might fit only a handful of VMs.

Side by side

AspectVirtual MachineContainer
Isolation unitFull guest OS on virtual hardwareProcess(es) sharing the host kernel
SizeGigabytesMegabytes
Startup timeSeconds to minutesMilliseconds to seconds
OverheadHigh (each VM runs a full OS)Low (no guest OS)
Density per hostA few to dozensDozens to hundreds
Isolation strengthVery strong (hardware-level)Strong (OS-level)
Runs a different OS kernel?Yes (e.g. Windows on Linux)No β€” shares host kernel
Best forStrong isolation, mixed OSes, legacy appsPortable apps, microservices, CI/CD, scale
Not either / or In the real world these two often stack: cloud providers run your containers inside VMs. You get the hardware isolation of a VM and the portability and density of containers at the same time.

What else is there, besides containers?

Containers are one point on a spectrum of ways to package and isolate software. It’s worth knowing the neighbors:

  • Bare metal β€” the app runs directly on a physical server. Fastest, but no isolation and hard to reproduce.
  • Virtual machines β€” strong isolation, heavy, described above.
  • Containers β€” lightweight OS-level isolation, the focus of this guide.
  • Serverless / Functions-as-a-Service β€” you supply only code; the platform handles running it (often on containers under the hood). Great for event-driven work, less control over the environment.
  • WebAssembly (Wasm) β€” an emerging, even lighter sandbox for running code securely across platforms; increasingly paired with containers rather than replacing them.
03 β€” Architecture

Docker architecture

Docker uses a client–server design. You type commands into a client; a background service does the real work; and a registry stores the images you share. Three moving parts, one clear picture.

The client talks to the daemon over an API Β· the daemon manages images & containers Β· images live in a registry

The Docker client

The client is the docker command you type in your terminal (or the Docker Desktop GUI). It doesn’t run containers itself β€” it translates your commands into API calls and sends them to the daemon. A single client can even point at a daemon running on a different machine.

The Docker host & the Docker daemon

The Docker host is the machine where containers actually run β€” your laptop, a server, or a cloud VM. Running on it is the Docker daemon (dockerd), the long-lived background service that is the real engine of Docker. The daemon:

  • listens for API requests from clients;
  • builds, stores, and manages images;
  • creates, starts, stops, and removes containers;
  • manages volumes and networks;
  • pulls images from, and pushes images to, registries.

The client and daemon together are the Docker Engine. Everything a container is β€” its images, its storage, its networking β€” is owned and orchestrated by this daemon on the host.

Docker registries

A registry is the warehouse for images. When you pull, the daemon downloads an image from a registry; when you push, it uploads one. Docker Hub is the default public registry β€” home to official images like nginx, postgres, and python. Organizations also run private registries (Amazon ECR, GitHub Container Registry, Google Artifact Registry, or a self-hosted one) to keep proprietary images internal.

Docker objects

Everything the daemon manages is a Docker object: images, containers, volumes, and networks. You’ll meet each in detail later β€” the key idea is that they all live on the host and are administered through the daemon.

one command, tracing the whole path
# You run this in your terminal (the CLIENT)...
docker run nginx

# 1. Client sends the request to the DAEMON (dockerd)
# 2. Daemon looks for the "nginx" IMAGE on the HOST
# 3. Not found? It pulls it from the REGISTRY (Docker Hub)
# 4. Daemon creates and starts a CONTAINER from that image

What makes it possible: three kernel features

Under all of this, Docker is a friendly interface over long-standing Linux kernel capabilities:

Namespaces
Give each container its own isolated view of the system β€” its own process list, network stack, mounts, and hostname. This is the isolation.
Control groups (cgroups)
Limit and meter how much CPU, memory, and I/O a container can use, so one container can’t starve the others. This is the resource control.
Union filesystems
Stack read-only image layers plus a thin writable layer, so images are built and stored efficiently as reusable layers. This is the packaging.

Namespaces isolate what a container can see, cgroups limit what it can consume, and the layered filesystem makes images small and fast to share.

Good to know On Windows and macOS there’s no Linux kernel to share, so Docker Desktop quietly runs a lightweight Linux VM to host the daemon. Your containers still feel local, but they’re running on that hidden Linux host.
04 β€” Vocabulary

Core concepts & vocabulary

Ten terms carry most of Docker. Learn these and the rest of the documentation reads easily.

Image
A read-only template β€” your app plus its dependencies β€” used to create containers. Think of it as a class.
Container
A running (or stopped) instance of an image. Think of it as an object created from that class.
Dockerfile
A text recipe of instructions that describes how to build an image, step by step.
Registry
A store for images. Docker Hub is the default public one; teams also run private registries.
Repository & Tag
A named collection of image versions, e.g. nginx:1.27 β€” nginx is the repo, 1.27 is the tag.
Layer
One step of an image’s filesystem. Cached and shared across images to save space and time.
Volume
Docker-managed storage that lives outside a container, so data survives when the container is removed.
Network
A virtual network that lets containers find and talk to each other by name.
Docker Engine
The core runtime: the daemon, the API, and the CLI that together build and run containers.
Docker Compose
A tool to define and run multi-container apps from a single YAML file.
The mental model A Dockerfile builds an image, which you push to a registry, then pull anywhere to run as a container. Persist state with volumes, connect services with networks, and orchestrate several at once with Compose.
05 β€” Setup

Installing Docker

On a laptop, install Docker Desktop. On a Linux server, install Docker Engine. Then verify with a one-line smoke test.

Windows & macOS β€” Docker Desktop

Download Docker Desktop from the official site and run the installer. It bundles the engine, CLI, Compose, and a GUI dashboard, and on Windows it runs Linux containers through WSL 2. After installing, launch it once and wait for the whale icon to say it’s running.

Windows tip Enable WSL 2 when prompted β€” it gives you a real Linux kernel and much better performance than the older Hyper-V backend.

Linux β€” Docker Engine (Ubuntu/Debian)

On a server you usually want the engine without the desktop GUI. The simplest path for a quick start is Docker’s convenience script; for production, follow the official apt-repository steps in the docs.

quick install (Linux)
# Fast path β€” Docker's official convenience script
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Run docker without sudo (log out/in afterwards)
sudo usermod -aG docker $USER

Verify the install

smoke test
docker --version          # prints the installed version
docker run hello-world   # pulls a tiny image and runs it

If hello-world prints a welcome message, your client, daemon, and registry access are all working. You’re ready to go.

06 β€” Reference

Essential commands

The commands you’ll reach for every day, grouped by what they act on. Bookmark this section.

Running & managing containers
docker run -d -p 8080:80 nginx
Run a container in the background, mapping host port 8080 to container port 80.
docker run -it ubuntu bash
Run interactively and open a shell inside the container.
docker ps
List running containers. Add -a to include stopped ones.
docker stop <id>
Gracefully stop a running container.
docker start <id>
Start a stopped container again.
docker restart <id>
Stop then start a container.
docker rm <id>
Remove a stopped container. Add -f to force-remove a running one.
docker exec -it <id> bash
Open a shell inside an already-running container.
docker logs -f <id>
View a container’s logs; -f follows them live.
Working with images
docker pull node:20
Download an image from a registry.
docker images
List images stored locally.
docker build -t myapp:1.0 .
Build an image from the Dockerfile in the current directory and tag it.
docker tag myapp:1.0 user/myapp:1.0
Add a new name/tag to an existing image (e.g. before pushing).
docker push user/myapp:1.0
Upload an image to a registry.
docker rmi <image>
Remove a local image.
docker history <image>
Show the layers that make up an image.
Inspecting & debugging
docker inspect <id>
Full low-level JSON details of a container or image.
docker stats
Live CPU, memory, and network usage per container.
docker top <id>
Show the processes running inside a container.
docker cp <id>:/path ./local
Copy files between a container and the host.
Cleaning up
docker system df
Show how much disk images, containers, and volumes are using.
docker container prune
Remove all stopped containers.
docker image prune
Remove dangling (untagged) images.
docker system prune -a
Aggressively reclaim space β€” removes unused images, containers, and networks.
Handy habit Docker keeps stopped containers and old images around, and they add up fast. Run docker system df when disk fills, then prune what you don’t need.
07 β€” Building

Writing a Dockerfile

A Dockerfile is the recipe for your image. Each line is an instruction; each instruction usually adds a layer.

The instructions you’ll use most

InstructionWhat it does
FROMThe base image to build on top of (must be the first instruction).
WORKDIRSets the working directory for later instructions.
COPYCopies files from your project into the image.
RUNExecutes a command at build time (e.g. install dependencies).
ENVSets an environment variable inside the image.
ARGDefines a build-time variable passed with --build-arg.
EXPOSEDocuments which port the app listens on.
CMDThe default command to run when a container starts (can be overridden).
ENTRYPOINTThe fixed executable a container runs; CMD supplies its default arguments.

A real example β€” a Node.js app

Dockerfile
FROM node:20-alpine          # small, official base image
WORKDIR /app                   # work inside /app

# Copy dependency manifests FIRST so this layer is cached
COPY package*.json ./
RUN npm ci --only=production

# Now copy the rest of the source
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]
Why the copy order matters Copying package.json and installing dependencies before copying your source means Docker reuses the cached dependency layer whenever only your code changes β€” turning minute-long rebuilds into second-long ones.

Build and run it

terminal
docker build -t my-node-app:1.0 .
docker run -d -p 3000:3000 my-node-app:1.0

Multi-stage builds (intermediate)

You often need heavy build tools to compile an app, but you don’t want them shipped in the final image. A multi-stage build compiles in one stage and copies only the finished artifact into a slim final stage.

Dockerfile β€” multi-stage
# --- build stage ---
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

# --- final stage: only the built output ships ---
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
.dockerignore Add a .dockerignore file (like .gitignore) listing node_modules, .git, logs, and secrets. It keeps builds fast and prevents junk β€” or credentials β€” from ending up in your image.
08 β€” Advanced internals

Images, layers & security

Once you’re comfortable running containers, a look under the hood at how images are pulled, layered, and secured pays off every day.

What actually happens when you run docker pull

Pulling an image isn’t a single file download. An image is described by a manifest β€” a small document listing the image’s configuration and its ordered layers, each identified by a cryptographic digest (a SHA-256 hash of its contents). Here’s the sequence:

  1. The client sends the pull request to the daemon.
  2. The daemon contacts the registry and authenticates if the image is private.
  3. The registry returns the image manifest β€” the list of layer digests plus the config.
  4. The daemon checks which of those layers it already has locally (by digest) and downloads only the missing ones.
  5. Each missing layer is downloaded compressed, verified against its digest, then decompressed and stored.
  6. The layers are assembled into the final image and tagged locally, ready to run.
docker pull nginx β€” annotated output
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete   # each line is ONE layer
c7a5c2b6e3f1: Pull complete
5f70bf18a086: # already had this layer β†’ not re-downloaded
Digest: sha256:1adf3...b21c   # the image's content address
Status: Downloaded newer image for nginx:latest
Why this is clever Because layers are addressed by their content, a layer shared across ten images is stored and downloaded once. Re-pulling after a small change downloads only the layers that actually changed.

How image layering works

Every instruction in a Dockerfile (FROM, RUN, COPY…) produces one read-only layer. A union filesystem stacks these layers into a single coherent view. When you start a container, Docker adds a thin writable layer on top β€” the container layer.

writable (per container) read-only (shared image layers)

Copy-on-write (COW)

The writable layer uses a copy-on-write strategy. Reading a file passes straight through to the layer below. But the first time a container modifies a file from a lower layer, that file is copied up into the writable layer and changed there β€” the original read-only layer is never touched. This is why:

  • Starting a container is instant β€” no copying, just a new thin writable layer on top of shared read-only ones.
  • A hundred containers from the same image share one copy of the read-only layers, so they’re cheap on disk.
  • Anything written to that writable layer vanishes when the container is removed β€” which is exactly why you use volumes for data you want to keep (next section).

Inspect the layers yourself

peek inside an image
docker history nginx          # see each layer and the instruction that made it
docker image inspect nginx    # full JSON: digests, layers, config
docker image ls --digests     # show the content-address (sha256) of images
Layer discipline Fewer, well-ordered layers build faster and stay smaller. Put things that rarely change (installing dependencies) early, and things that change often (copying source) late β€” so the cache is reused as much as possible.

Scanning images for security vulnerabilities

An image bundles an OS userland and your dependencies β€” and any of those can carry known vulnerabilities (published as CVEs). A base image that was clean six months ago may have several critical CVEs today. Scanning compares every package in your image’s layers against vulnerability databases and reports what’s exposed.

Docker Scout β€” built in

docker scout
docker scout quickview myapp:1.0   # summary of vulnerabilities by severity
docker scout cves myapp:1.0        # detailed CVE list with affected packages
docker scout recommendations myapp:1.0  # suggested base-image upgrades

Popular third-party scanners

Trivy
Fast, open-source scanner from Aqua Security; scans images, filesystems, and IaC. A CI favorite.
Grype
Open-source vulnerability scanner from Anchore, pairs with the Syft SBOM tool.
Snyk
Commercial platform with deep dependency analysis and fix guidance, integrates with Docker.
scan with Trivy
trivy image myapp:1.0
trivy image --severity HIGH,CRITICAL myapp:1.0   # only what matters most
Make it a habit Scan images automatically in your CI pipeline and fail the build on new critical CVEs. Combined with slim base images and regular rebuilds on updated bases, this keeps what you ship genuinely secure β€” not just working.
09 β€” Persistence

Data & volumes

Containers are disposable by design. Anything written inside one disappears when it’s removed β€” unless you store it outside the container.

Docker gives you two main ways to persist and share data:

  • Volumes β€” storage that Docker manages in its own area. Best for databases and app data you want to keep. Portable and the recommended default.
  • Bind mounts β€” map a folder on your host straight into the container. Perfect in development, so code changes on your machine appear instantly inside the container.
volumes & bind mounts
# Named volume β€” persist a database across restarts
docker volume create pgdata
docker run -d -v pgdata:/var/lib/postgresql/data postgres:16

# Bind mount β€” live-edit local code inside the container
docker run -v $(pwd):/app -p 3000:3000 my-node-app

# Housekeeping
docker volume ls
docker volume rm pgdata
Rule of thumb Named volumes for data you must keep (databases, uploads). Bind mounts for source code during development. Never rely on the container’s own writable layer for anything you care about.
10 β€” Connectivity

Networking basics

Real apps are several containers talking to each other β€” a web server, a database, a cache. Docker networks make that clean.

By default, containers on the same user-defined network can reach each other by container name, no IP addresses required. The main network types are:

  • bridge β€” the default; containers on the same bridge network talk to each other privately. This is what you’ll use most.
  • host β€” the container shares the host’s network directly (no isolation, occasional performance use).
  • none β€” no networking at all, for fully isolated workloads.
connect two containers
# Create a network
docker network create appnet

# Start a database on it, named "db"
docker run -d --name db --network appnet postgres:16

# The app can now reach the database at hostname "db"
docker run -d --name web --network appnet -p 3000:3000 my-node-app

Inside the web container, the database is simply reachable at the host db β€” Docker’s built-in DNS resolves the name to the right container. This name-based discovery is exactly what Docker Compose automates for you next.

11 β€” Multi-container

Docker Compose

Typing long docker run commands for every service gets old fast. Compose defines your whole stack in one file and brings it up with a single command.

You describe your services, networks, and volumes in a compose.yaml (or docker-compose.yml) file, then run docker compose up. Compose creates a shared network automatically, so services find each other by name.

compose.yaml β€” web app + database
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
everyday compose commands
docker compose up -d      # build + start the whole stack in the background
docker compose ps         # see what's running
docker compose logs -f     # tail logs from all services
docker compose down       # stop and remove everything
docker compose down -v    # ...and delete the volumes too
Why teams love it A new developer clones the repo and runs one command β€” docker compose up β€” to get the app, its database, and every dependency running locally, wired together and identical to everyone else’s setup.
12 β€” In practice

Real-world scenarios

Where Docker earns its keep, ordered from your first day to intermediate workflows.

Beginner

Try any tool without installing it

Want to test PostgreSQL, Redis, or a specific Python version without polluting your machine? Run it as a throwaway container. When you’re done, remove it and your system is clean.

disposable services
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16
docker run -d -p 6379:6379 redis:7
Beginner

A consistent development environment

Package your app and its dependencies so every teammate β€” and every CI run β€” uses the exact same versions. No more β€œupdate your Node version” threads. This is the everyday payoff of Compose from the previous section.

Intermediate

Continuous integration & delivery (CI/CD)

CI systems (GitHub Actions, GitLab CI, Jenkins) build your image, run the test suite inside a fresh container, and β€” if tests pass β€” push the image to a registry ready to deploy. Because the build environment is a container, it’s identical every time, eliminating β€œpasses locally, fails in CI.”

Intermediate

Microservices

Split a large application into small, independently deployable services β€” each in its own container, each scalable on its own. Docker is the packaging unit; an orchestrator like Kubernetes runs many of them across a cluster.

Intermediate

Reproducible data science & ML

Pin Python, CUDA, and every library into an image so an experiment runs the same on a laptop, a GPU server, or a colleague’s machine months later. Reproducibility becomes a property of the image, not a hope.

Intermediate

Deploying a web app to a server

Build your image, push it to a registry, then on any cloud host pull and run it behind a reverse proxy. The same image you tested locally is the one running in production β€” no drift, no surprises.

13 β€” Craft

Best practices

Small habits that separate a rough Docker setup from a professional one.

Keep images small
Start from slim bases like -alpine or -slim, and use multi-stage builds so build tools never ship.
Order layers for caching
Copy dependency manifests and install before copying source, so code changes don’t invalidate the dependency layer.
Use a .dockerignore
Exclude node_modules, .git, logs, and secrets from the build context.
One concern per container
Run a single main process per container. Need a database too? That’s a second container.
Don’t run as root
Add a non-root USER in your Dockerfile to reduce the blast radius if a container is compromised.
Never bake in secrets
Pass credentials via environment variables or secret managers at runtime β€” never COPY them into an image.
Tag deliberately
Pin real versions (node:20.11) instead of relying on latest, which silently changes.
Scan & update
Regularly scan images for vulnerabilities and rebuild on updated bases to pick up security patches.
14 β€” The wider world

Beyond Docker

Docker made containers mainstream, but it’s part of a larger ecosystem. Knowing the neighbors helps you make sense of job posts and architecture diagrams.

Other container tools

ToolWhat it is
containerdThe lightweight runtime that actually runs containers β€” Docker uses it under the hood, and so does Kubernetes.
PodmanA daemonless, Docker-compatible alternative that can run containers without a background service or root.
BuildahA focused tool for building container images, often paired with Podman.
OCIThe Open Container Initiative β€” the open standards for image and runtime formats that keep all these tools interoperable.

Orchestration β€” running containers at scale

One host running a handful of containers is easy. Running hundreds across many machines β€” with automatic restarts, scaling, rolling updates, and load balancing β€” needs an orchestrator.

  • Kubernetes (K8s) β€” the industry-standard orchestrator for production container fleets. This is the natural next thing to learn after Docker.
  • Docker Swarm β€” Docker’s own, simpler orchestrator; easy to start with, less common at large scale.
  • Managed services β€” cloud offerings (e.g. AWS ECS/EKS, Google GKE, Azure AKS) that run orchestrated containers so you manage less infrastructure.
Your learning path Master Docker images, containers, volumes, networks, and Compose first. Once multi-container apps feel natural, move on to Kubernetes β€” everything you learned here about images and containers carries straight over.