Part 10 · Architecture & Deployment
Docker & Containers for Beginners
“It works on my machine” ends here. What containers really are (and aren't), images, layers and registries, a production-grade multi-stage Dockerfile, running and composing containers locally, tags vs digests, and the security basics.
On this page
The Problem
"It works on my machine!"
Your Node.js app runs perfectly on your laptop. On the test server, it crashes because the server has a different Node version. In production, an image library needs a system package that is not installed. A new teammate spends two days installing the right versions of PostgreSQL, Redis, Python and a PDF tool just to run the project on their laptop.
Now imagine you want to run 20 copies of a service on a few big servers. If you install everything directly on each machine, the versions start to clash and old files are left behind.
A container is a standard package that holds an application together with everything it needs to run (its runtime, libraries and settings). It runs the same way on every machine. Docker is the most popular tool for building and running containers. It made containers easy to use, and today they are the default way to ship software.
The Core Idea
Think about shipping containers in global trade.
Before standard shipping containers, loading a ship was slow and messy. Sacks, barrels, crates and loose goods of every shape had to be handled one by one, and each port did it differently.
Then came the standard steel container. Whatever is inside (bananas, cars, electronics), the container is the same size and shape. Cranes, ships, trains and trucks all know how to move it. Nobody needs to know what is inside to carry it.
Software containers work the same way:
- Inside: your app, its runtime (the program that runs your code, like Node 24 or Python 3.12), its libraries and its settings.
- Outside: a standard format that any container platform (a laptop, a CI server, Kubernetes, a cloud service) knows how to run.
How It Works
Key terms in plain words
- Kernel: the core part of an operating system. It controls the CPU, memory, disk and network for all programs.
- Virtual machine (VM): a pretend computer, run by software, that has its own full operating system. A hypervisor is the software that creates and runs VMs.
- Container: a normal process on a machine that is isolated (kept apart) from other processes. It looks like a small computer to the app inside, but it uses the host's kernel.
- Image: a read-only package that contains your app and everything it needs. A Dockerfile is a text file with the steps to build an image.
- Registry: a server that stores images so that other machines can download (pull) them.
- Volume: a folder that lives outside the container, so its data stays after the container is deleted.
- stdout and stderr: the two normal output streams of a program. Normal messages go to stdout. Error messages go to stderr.
- Root: the all-powerful user account on Linux. It can change anything on the machine.
- Environment variable: a named setting (like
DATABASE_URL) that you give to a program from outside, when it starts. The program reads it while it runs. - Lockfile: a file (like
package-lock.json) that records the exact version of every library your project uses, so every install gets the same versions. - YAML: a plain-text format for settings, written as indented
key: valuelines. Docker Compose files use it. - Port: a number that picks one network door on a machine.
8080:3000means "machine port 8080 leads to container port 3000".
Containers vs virtual machines
- •Hypervisor on host hardware
- •Each VM boots its own guest OS
- +Strong isolation — separate kernels
- −Gigabytes each; minutes to boot
- •Container runtime on one host kernel
- •Each app packs only its libraries
- +Megabytes; start in milliseconds
- +Dense packing, identical everywhere
- −Shared kernel = weaker isolation
| Virtual machine | Container | |
|---|---|---|
| What's inside | A full operating system + app | Just the app + its libraries |
| Kernel | Each VM has its own | Shares the host's kernel |
| Size | Gigabytes | Often tens to hundreds of MB |
| Start time | Tens of seconds to minutes | Milliseconds to seconds |
| Density | Fewer per server | Many per server |
| Isolation | Strong (hardware-level) | Good, but weaker (shared kernel) |
How containers work on Linux:
- Namespaces are a Linux feature that gives each container its own view of the system: its own process list, network interfaces, file system, hostname and users. The container "thinks" it is alone on the machine.
- cgroups (control groups) are a Linux feature that limits resources. For example: "this container can use at most 1 CPU and 512 MB of memory".
- A layered file system (like OverlayFS) stacks the read-only image layers on top of each other. A thin writable layer goes on the very top. A "layer" is one set of file changes made by one Dockerfile step.
Containers are isolated processes, not tiny virtual machines. That is why they are fast and light. (If you need stronger isolation, you can use gVisor, which adds an extra layer between the container and the kernel, or Firecracker, which starts very small and fast VMs.)
Images, containers and registries
- Image: a read-only template. It holds your app plus everything it needs, and you build it from a Dockerfile. Think of it as a recipe with all the ingredients, packed in a box.
- Container: a running copy of an image. It is the dish that was cooked from the recipe. You can run many containers from one image.
- Registry: a place to store and share images. Examples are Docker Hub, GitHub Container Registry (GHCR), Amazon ECR and Google Artifact Registry.
- Dockerfile
- Imageimmutable layers
- RegistryDocker Hub, ECR, GHCR
- Run anywherelaptop, CI, Kubernetes
A Dockerfile, step by step
Here is a simple Dockerfile for a Node.js API. It is not optimised yet. Each line is an instruction: FROM picks the base image, WORKDIR sets the folder, COPY copies files in, RUN runs a command while building, EXPOSE documents the port, and CMD is the command that starts the app.
FROM node:24WORKDIR /appCOPY . .RUN npm installEXPOSE 3000CMD ["node", "server.js"]It works, but the image is large (the full Node image plus dev tools). Also, every code change reinstalls all dependencies.
A better Dockerfile is a multi-stage one. It uses more than one FROM. The first stage builds the app. The second stage is the small image that you ship. (npm ci installs the exact versions from the lockfile, and it fails if the lockfile does not match package.json.)
# ---- Stage 1: build ----FROM node:24-slim AS buildWORKDIR /app# Copy the dependency files FIRST (so Docker can cache this step)COPY package*.json ./# Install the exact versions listed in the lockfileRUN npm ciCOPY . .# For example, compile TypeScriptRUN npm run build# Remove the dev dependencies (tools only needed to build)RUN npm prune --omit=dev# ---- Stage 2: run ----FROM node:24-slimWORKDIR /appENV NODE_ENV=productionCOPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./dist# Do not run as root (the all-powerful user)USER nodeEXPOSE 3000CMD ["node", "dist/server.js"]Why this is better:
- Layer caching. Docker saves (caches) the result of each step and reuses it if nothing changed. Here
package.jsonis copied before the source code. So dependencies are only reinstalled when they change, not on every code edit. Builds can go from minutes to seconds. - Multi-stage builds. Compilers, dev dependencies and build tools stay in the first stage. The final image contains only what is needed to run. So it is smaller, faster to download and has less for an attacker to target.
- Slim base images. A base image is the image you start from (the
FROMline). Small ones, such as-slim,alpineor distroless (almost nothing except your app and its runtime), reduce size and the number of known security holes. - A non-root user. If an attacker breaks into the app, they do not get root power inside the container.
A .dockerignore file tells Docker which files to leave out of the image (so they are not copied by COPY . .):
node_modules.git.env*.logcoverage⚠️ Never put secrets in images (like .env files, API keys or COPY credentials.json). Anyone who downloads the image can read them, even from old layers. Pass secrets when the container starts instead (post 51).
Running containers
docker build -t myshop/api:1.4.2 .# -p: connect port 8080 on your machine to port 3000 in the container# -e: pass settings as environment variables# --memory / --cpus: limits on memory and CPU (these use cgroups)docker run -d --name api \ -p 8080:3000 \ -e DATABASE_URL=postgres://... \ --memory 512m --cpus 1 \ myshop/api:1.4.2docker ps # list the running containersdocker logs -f api # follow the logs (your app should write logs to stdout)docker exec -it api sh # open a shell inside the container (for debugging)docker stop apiKey ideas:
- Containers are disposable. Anything written inside the container disappears when the container is removed. Store data in volumes, databases or object storage (post 17).
- Volumes keep data outside the container, so the data stays after the container is gone:
-v pgdata:/var/lib/postgresql/data. - Configuration comes from the environment, not from inside the image (this is an idea from the Twelve-Factor App, a well-known list of good practices for web apps). One image can move through dev, staging and production with different settings.
- Logs go to stdout and stderr. The platform collects them from there (post 46).
- −FROM node:24 — a large base image
- −COPY . . before npm install — every code change re-installs dependencies
- −Dev dependencies and build tools shipped to production
- −Runs as root
- +Slim base image; build stage separate from runtime stage
- +Copy package files first → cached dependency layer
- +npm ci from the lockfile; prune dev dependencies
- +USER node — never root
Tags vs digests
- Tags like
myshop/api:1.4.2or:latestare labels that can be moved to point at a different image later. - Digests like
myshop/api@sha256:9f86d0…identify one exact image forever. A digest is a hash (a fingerprint) of the image content.
Avoid :latest in production. You cannot tell which version is running, and the image behind the tag can change without warning. Use version tags (or git commit SHAs). For the most safety, deploy by digest.
Docker Compose for local development
Docker Compose is a tool that runs a whole set of containers from one YAML file, with one command. It solves the "new teammate spends two days installing things" problem:
# docker-compose.ymlservices: api: build: . ports: ["8080:3000"] environment: DATABASE_URL: postgres://app:secret@db:5432/shop REDIS_URL: redis://cache:6379 depends_on: [db, cache] db: image: postgres:16 environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: shop volumes: ["pgdata:/var/lib/postgresql/data"] cache: image: redis:7volumes: pgdata:docker compose up # starts the API, PostgreSQL and Redis, all connected on one networkServices find each other by name (db, cache). This is service discovery in a small form (post 54). Note that depends_on only controls the start order. It does not wait until the database is ready to accept connections. (The passwords above are only for local development. Never use them in real environments.)
The container ecosystem and standards
- The Open Container Initiative (OCI) is a group that defines standard image and runtime formats. Because of this, images built with Docker run on any platform that follows the standard.
- containerd and runc are the low-level programs that actually run containers. Docker uses them inside, and so does Kubernetes.
- Podman is a popular Docker alternative. It can run without a daemon (a program that always runs in the background) and as a non-root user.
- Kubernetes (post 56) is a system that starts, stops and manages containers across many machines. It runs OCI images and does not need Docker itself on the servers.
Container security basics
- ✅ Minimal base images. Rebuild regularly to get security patches.
- ✅ Scan images for known security holes (vulnerabilities) with tools like Trivy or Grype. Do it in CI, the automatic build-and-test step (post 52).
- ✅ Run as non-root. Remove Linux capabilities you do not need (a capability is one small special power of root). Use read-only file systems where you can.
- ✅ No secrets in images. Inject them at runtime.
- ✅ Pin versions of base images (use an exact version, not "latest"). Also sign images, for example with Sigstore/cosign, so you can check who made what you deploy.
- ✅ Set resource limits, so one container cannot take all the CPU or memory from the others (these are the bulkheads from post 42).
Trade-offs
- ✅ Consistency: the same image runs everywhere, which ends "works on my machine".
- ✅ Speed and density: fast starts and many containers per server, ideal for scaling and microservices.
- ✅ Isolation of dependencies: different apps can use different versions side by side.
- ✅ Standard packaging for CI/CD, Kubernetes and cloud platforms.
- ❌ Weaker isolation than VMs (the kernel is shared). If many untrusted customers share one machine (multi-tenant) and the data is very sensitive, you may need sandboxes or VMs.
- ❌ New skills and tooling: images, registries, networking and storage.
- ❌ Stateful workloads (databases) need careful handling of volumes and backups.
- ❌ Image sprawl and vulnerabilities if images aren't maintained and scanned.
When you might not need containers: simple apps on a fully managed platform. A PaaS (platform as a service) or serverless platform builds and runs your code for you. Often it uses containers inside, but you do not have to manage them.
In the Real World
Docker's launch (2013). Solomon Hykes demonstrated Docker at PyCon in 2013, and it spread very fast. The base technology (Linux namespaces and cgroups) already existed. Docker made it easy, with a simple command-line tool, Dockerfiles and a public image registry (Docker Hub).
Google and containers. Long before Docker, Google ran almost everything in containers on its internal cluster manager, Borg. Google has said it started billions of containers per week. Google engineers wrote much of the first Linux cgroups feature. The lessons from Borg later shaped Kubernetes (post 56).
The OCI standard (2015). Docker and other companies created the Open Container Initiative, so that container images and runtimes would not belong to one vendor. That is why an image built on your laptop runs on AWS, Google Cloud, Azure or your own Kubernetes cluster.
Dev environments with Compose. Many companies ship a docker-compose.yml with each repository, so new developers can run the whole system on their laptop with one command. Onboarding (getting a new person started) goes from days to minutes.
Kubernetes dropping "dockershim" (2022). Kubernetes removed its built-in Docker Engine integration in version 1.24. This caused confusion ("Is Docker dead?"). But images built with Docker kept working, because Kubernetes runs OCI images through containerd or CRI-O. It shows why open standards matter.
Interview Questions
Q1What's the difference between a container and a virtual machine?
A VM pretends to be a whole computer and runs its own guest operating system and kernel. So it is heavy but strongly isolated. A container is an isolated process that uses the host's kernel. Namespaces give the isolation and cgroups give the limits. So it is light and starts fast, but the isolation is weaker.
Q2What are image layers, and why does Dockerfile order matter?
Each instruction creates a cached, read-only layer. When one layer changes, it and every layer after it are rebuilt. So copy the dependency files and install dependencies before you copy the source code. Then a code change does not reinstall the dependencies.
Q3Why use multi-stage builds?
You build with all the tools in one stage. Then you copy only the built output and the runtime dependencies into a small final image. This gives smaller images, less for an attacker to target and faster downloads.
Q4Tags vs digests?
A tag like :1.4.2 or :latest is a label that can be moved to different content. A digest (sha256:…) is a fingerprint of exact content that cannot change. Deploy by digest, or at least by version tags that you never move. Never deploy :latest.
Q5How should a containerised app handle config and logs?
Config and secrets come in at start time, through environment variables or mounted secret files. They are never built into the image. Logs go to stdout and stderr, and the platform collects them. The container itself keeps no data (it is stateless) and can be thrown away.
Q6What are basic container security practices?
Use minimal base images. Run as a non-root user. Remove Linux capabilities you do not need. Use a read-only file system where possible. Scan images for vulnerabilities. Sign images and check the signatures before you deploy. Never put secrets into image layers.
Key Takeaways
- A container packages an app with its dependencies into a standard, portable unit. It's an isolated process (namespaces + cgroups) sharing the host kernel. This makes it much lighter than a VM.
- Image = template, container = running instance, registry = where images are stored.
- Write good Dockerfiles:
- copy dependency files first (layer caching),
- use multi-stage builds and slim or distroless bases,
- run as non-root,
- add a
.dockerignore, - and never bake in secrets.
- Containers are disposable: keep data in volumes or databases, config in environment variables, and logs on stdout. Use version tags or digests, not
:latest. - Use Docker Compose for local multi-service setups, scan and sign images, and rely on OCI standards for portability.
Further Reading
- The official Docker documentation: "Get started" and "Building best practices"
- The Open Container Initiative (OCI) specifications
- The Docker Compose documentation
- The OWASP Docker Security Cheat Sheet
- Julia Evans' article "What even is a container?"
- The Twelve-Factor App (factors on config, logs and disposability)
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series