Docker Tutorial for Beginners: Image, Container, Dockerfile
Learn Docker from a real Dockerfile: what an image and a container are, how layers and the build cache work, and how to order instructions for fast builds.
By Matthews WongReviewed on
Key takeaway
A Docker image is a read-only stack of layers, built from the instructions in a Dockerfile; a container is a running copy of that image with one thin writable layer on top. Docker caches each layer, so ordering instructions from least to most often changed is what keeps rebuilds fast.
You will be able to
- Explain the difference between an image, a layer and a container.
- Predict which steps of a Docker build come from cache after you edit a file.
- Order a Dockerfile and a .dockerignore so builds are fast and secrets stay out.
Step through the diagram
Press Play, or step with the arrows. Every step is written out below the diagram.
How a Docker image is built, layer by layer
The build context goes to docker build, each Dockerfile instruction adds a layer to the image, an edit rebuilds only the layers after it, and running the image adds a writable layer.
Keys: ← → step · Home / End · K play or pause
A Dockerfile is a list of instructions. Most of them add one read-only layer to the image, and the layers stack from the bottom up.
docker build first sends the build context. .dockerignore keeps node_modules, .git and .env out, so they can never end up in the image.
FROM starts from an existing image. Its layers are reused exactly as they are; nothing in them is copied or changed.
FROM node:20-alpine AS depsCopy only the dependency files, then install. This layer changes only when package-lock.json changes, and a cache mount keeps downloaded packages between builds.
COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm npm ciNow copy the source code and build it. Source changes often, so these instructions come last in the file.
COPY . . RUN npm run buildPredict the next step You edit one source file and build again. What runs again? (Every instruction, from FROM down · COPY . . and everything after it · Only RUN npm ci)Edit one file and build again. The base and install layers come from cache; COPY . . and every layer after it run again.
Predict the next step What does running the image add to it? (A thin writable layer on top · A writable copy of every layer)Running the image adds one thin writable layer on top. The image layers stay untouched, so many containers can share them.
What is the difference between a Docker image and a container?
An image is a read-only template: a stack of layers, where each layer is a set of filesystem changes left by one Dockerfile instruction. Once a layer is created it never changes. A container is what you get when you run an image: Docker puts one thin writable layer on top, and everything the running process writes lands there.
Read more (1)Show less
That split is why ten containers from one image cost little extra disk: they share the same read-only layers and each keeps only its own writable layer. It is also why anything a container writes disappears when the container is removed, unless you mount a volume.
Why does the order of a Dockerfile decide how fast it rebuilds?
Docker caches each layer. On the next build it reuses a layer when that instruction and its inputs have not changed. But invalidation only flows downstream: when one layer changes, every layer after it is rebuilt too, even if those instructions are identical.
Read more (1)Show less
So the rule is to order instructions from the least to the most often changed. Dependencies change rarely and take the longest to install, so copy only package.json and package-lock.json and run the install before you copy the rest of the source. A one-line code change then reuses the install layer and rebuilds in seconds.
What does .dockerignore protect you from?
Before building, docker build sends the build context: the set of files the build can access, normally the project folder. Anything in it can be copied into the image by a careless COPY . . instruction. A .dockerignore file removes paths from the context, so node_modules, .git and .env files never reach the builder at all.
Read more (1)Show less
The same file has a sharp edge. A pattern like *.md matches Markdown files in the context root only; files in subfolders are still included. Exceptions start with !, and the last matching line wins, so an exception placed before the rule it overrides does nothing.
Why npm ci, and what does a cache mount add?
npm ci installs exactly what package-lock.json says, so every build gets the same dependency tree. npm install may update the lockfile and let builds drift. In a Dockerfile you want the reproducible one.
Read more (1)Show less
A cache mount, written RUN --mount=type=cache, gives that one instruction a persistent folder that lives outside the image layers. Point it at npm's download cache and a lockfile change still re-runs npm ci, but npm re-uses the packages it already downloaded instead of fetching every tarball again.
The real config behind this lesson
Excerpts from the repository that builds and deploys this website, quoted as they are.
# syntax=docker/dockerfile:1
# The syntax directive is what enables `--mount=type=cache` below.
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# Cache npm's download directory, not node_modules. This survives within a build
# even when the layer above is invalidated, so a lockfile change re-links from
# cache instead of re-downloading every tarball. Still `npm ci`, so the lockfile
# stays authoritative.
RUN --mount=type=cache,target=/root/.npm npm ciThe first stage of this site's image. Only the two dependency files are copied before the install, so editing a page never re-runs npm ci. The cache mount points at npm's download folder, not node_modules: a lockfile change still installs cleanly, it just stops re-downloading everything. Line 1 is what turns on the --mount syntax.
node_modules
.git
.env
.env.*
.DS_Store
npm-debug.log*
*.logThe first lines of this site's .dockerignore. Local dependencies, the git history and every .env file are kept out of the build context, so a COPY . . later in the Dockerfile can never copy a secret into the image.
# NOT excluded, deliberately — each of these looks like dead weight and is not:
# public/ 78 MB, but the runner stage copies it into the final image
# messages/ 12 MB, compiled into the server bundle by next-intl
# scripts/ 10 MB, because `prebuild` runs generate-blog-sources.mjs --check,
# check-placements.mjs and check-imports.mjsThe other half of the rule. These folders look heavy, and excluding them would make the build fail or the site break. Before you ignore something large, check whether the build reads it.
Mistakes that cost real time
COPY . . before the install
Copying the whole project before npm ci ties the install layer to every source file, so a one-line change reinstalls all dependencies. Copy the manifests, install, then copy the rest.
A secret baked into a layer
Without a .dockerignore, an .env file rides in with COPY . . and stays in that layer for good. Deleting it in a later instruction does not help: the earlier layer still holds it. Keep secrets out of the context and pass them at runtime.
Expecting a cache mount to survive in CI
On a fresh CI runner the cache mount starts empty, and the GitHub Actions cache does not save it by default. This site works around that for Next's compile cache with a .dockerignore exception, !.next/cache, so the restored cache rides in with the build context.
What I'd change here: the base image
This site still builds on node:20-alpine, and Node 20 reached end of life on 30 April 2026. A base image stops getting security fixes when its runtime does, so the next real edit to this Dockerfile is moving to a supported Node line.
| Image | Container | |
|---|---|---|
| What it is | A read-only stack of layers | A running copy of an image |
| Can it change? | No: you build a new image instead | Yes, in its writable layer |
| Created by | docker build | docker run |
| When removed | Layers go once no image uses them | Its writable layer is gone |
Do it on your own machine
You need Docker and Node installed, nothing else. Make a folder with a tiny server, build it twice, and watch which steps come from cache.
Create package.json and src/server.js, then run npm install once to create package-lock.json.
{ "name": "devops-lab", "version": "1.0.0", "private": true } // src/server.js require("node:http") .createServer((req, res) => res.end("hello\n")) .listen(3000, "0.0.0.0");Write a Dockerfile that installs dependencies before copying the source, and a .dockerignore that keeps node_modules out.
# syntax=docker/dockerfile:1 FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm npm ci COPY . . CMD ["node", "src/server.js"] # .dockerignore node_modules .gitBuild it, edit the server, and build again. The npm ci step shows CACHED; COPY . . runs again.
docker build -t devops-lab:v1 . echo "// tweak" >> src/server.js docker build -t devops-lab:v1 .List the layers with their sizes, then run a container and open http://localhost:8080.
docker history devops-lab:v1 docker run --rm -p 8080:3000 devops-lab:v1
Three questions before you move on
Pick an answer to see why it is right or wrong. Nothing is saved or graded, so guess freely.
Answer in your head, then check
1.You change one line in a component. Which Dockerfile instructions run again?Show the answer
Answer:COPY . . and everything after it. Invalidation only flows downstream, and the install layer depends only on package.json and package-lock.json.
2.Why copy package.json and package-lock.json before COPY . .?Show the answer
Answer:So the slow install layer depends only on the files that change least. Docker's own guidance is to put the steps that change often near the end of the Dockerfile.
3.This site's .dockerignore contains *.md. Does that exclude docs/guide.md?Show the answer
Answer:No. *.md matches Markdown files in the context root only; files in subfolders are still included. The site excludes the docs folder on its own line.
Related deep dives on this site
Sources
Primary sources checked on the research date. Follow them for the current version.