Skip to main content
DevOps from zero · Lesson 3 of 8 · 11 min

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.
Watch it work

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.

Step 1 of 7. 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.

Keys: ← → step · Home / End · K play or pause

  1. 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.

  2. docker build first sends the build context. .dockerignore keeps node_modules, .git and .env out, so they can never end up in the image.

  3. 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 deps
  4. Copy 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 ci
  5. Now 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)
  6. 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)
  7. 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)

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)

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)

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)

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.

How this site does it

The real config behind this lesson

Excerpts from the repository that builds and deploys this website, quoted as they are.

Dockerfile:1-11
# 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 ci

The 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.

.dockerignore:4-10
node_modules
.git
.env
.env.*
.DS_Store
npm-debug.log*
*.log

The 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.

.dockerignore:37-41
# 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.mjs

The 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.

What breaks

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 or container: what each one is
What it isA read-only stack of layersA running copy of an image
Can it change?No: you build a new image insteadYes, in its writable layer
Created bydocker builddocker run
When removedLayers go once no image uses themIts writable layer is gone
Try it yourself

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.

  1. 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");
  2. 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
    .git
  3. Build 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 .
  4. 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
Quick quiz

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.

  1. 1.A Dockerfile runs COPY . . and then RUN npm ci. You fix a typo in one component and rebuild. What happens to the install step?
  2. 2.There is no .dockerignore, and an .env file slipped into the project. The Dockerfile runs COPY . . and then RUN rm .env. Is the secret out of the image?
  3. 3.Locally, a lockfile change still builds fast because the cache mount keeps npm's downloads. On a fresh GitHub Actions runner, every build downloads every package again. Why?
Think it through

Answer in your head, then check

  1. 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. 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. 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.

Go deeper

Related deep dives on this site