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.
Jump to a chapter
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
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
| Aspect | Virtual Machine | Container |
|---|---|---|
| Isolation unit | Full guest OS on virtual hardware | Process(es) sharing the host kernel |
| Size | Gigabytes | Megabytes |
| Startup time | Seconds to minutes | Milliseconds to seconds |
| Overhead | High (each VM runs a full OS) | Low (no guest OS) |
| Density per host | A few to dozens | Dozens to hundreds |
| Isolation strength | Very strong (hardware-level) | Strong (OS-level) |
| Runs a different OS kernel? | Yes (e.g. Windows on Linux) | No β shares host kernel |
| Best for | Strong isolation, mixed OSes, legacy apps | Portable apps, microservices, CI/CD, scale |
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.
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.
# 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 isolate what a container can see, cgroups limit what it can consume, and the layered filesystem makes images small and fast to share.
Core concepts & vocabulary
Ten terms carry most of Docker. Learn these and the rest of the documentation reads easily.
nginx:1.27 β nginx is the repo, 1.27 is the tag.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.
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.
# 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
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.
Essential commands
The commands you’ll reach for every day, grouped by what they act on. Bookmark this section.
-a to include stopped ones.-f to force-remove a running one.-f follows them live.docker system df when disk fills, then prune what you don’t need.
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
| Instruction | What it does |
|---|---|
FROM | The base image to build on top of (must be the first instruction). |
WORKDIR | Sets the working directory for later instructions. |
COPY | Copies files from your project into the image. |
RUN | Executes a command at build time (e.g. install dependencies). |
ENV | Sets an environment variable inside the image. |
ARG | Defines a build-time variable passed with --build-arg. |
EXPOSE | Documents which port the app listens on. |
CMD | The default command to run when a container starts (can be overridden). |
ENTRYPOINT | The fixed executable a container runs; CMD supplies its default arguments. |
A real example β a Node.js app
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"]
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
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.
# --- 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 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.
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:
- The client sends the pull request to the daemon.
- The daemon contacts the registry and authenticates if the image is private.
- The registry returns the image manifest β the list of layer digests plus the config.
- The daemon checks which of those layers it already has locally (by digest) and downloads only the missing ones.
- Each missing layer is downloaded compressed, verified against its digest, then decompressed and stored.
- The layers are assembled into the final image and tagged locally, ready to run.
$ 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
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)
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
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
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 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 image myapp:1.0 trivy image --severity HIGH,CRITICAL myapp:1.0 # only what matters most
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.
# 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
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.
# 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.
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.
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:
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
docker compose up β to get the app, its database, and every dependency running locally, wired together and identical to everyone else’s setup.
Real-world scenarios
Where Docker earns its keep, ordered from your first day to intermediate workflows.
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.
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16 docker run -d -p 6379:6379 redis:7
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.
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.β
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.
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.
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.
Best practices
Small habits that separate a rough Docker setup from a professional one.
-alpine or -slim, and use multi-stage builds so build tools never ship.node_modules, .git, logs, and secrets from the build context.USER in your Dockerfile to reduce the blast radius if a container is compromised.COPY them into an image.node:20.11) instead of relying on latest, which silently changes.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
| Tool | What it is |
|---|---|
| containerd | The lightweight runtime that actually runs containers β Docker uses it under the hood, and so does Kubernetes. |
| Podman | A daemonless, Docker-compatible alternative that can run containers without a background service or root. |
| Buildah | A focused tool for building container images, often paired with Podman. |
| OCI | The 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.