Learning paths / Containers / Writing Dockerfiles

Anatomy of a Dockerfile

Video and reading · 13 min · Module 4, lesson 1 of 659 min left in this module

Module 4 · Writing DockerfilesLesson 1 of 6

Goal: Read every instruction in a Dockerfile and say what each one adds to the image.

3:37 · captions and chapters · narrated with an AI-generated voice
Transcript

Narration uses an AI-generated voice.

[00:00] Where we're going

By the end of this video, you'll be able to read a Dockerfile, and say what each line adds to the image. A Dockerfile is a recipe for an image. Docker reads it from top to bottom. And every instruction does one of two things. It adds a layer of files, or it records a setting.

[00:21] The base and the folder

Here's the one from the lesson. It packages a small Node web app. The first line picks the base image. This one already has Node twenty-two installed. Everything else is built on top of it. Next, the working directory. Every instruction after this one runs in a folder called app.

[00:41] Dependencies, then source

Then a copy. It brings in just the dependency list: the package file, and its lock file. Run executes a command while the image is being built. Here, it installs those dependencies. Only then does the second copy bring in the rest of the source. Each of these three adds a layer of files. And the order is on purpose. You'll see why in a minute.

[01:07] Settings for whoever runs it

The last four lines add no files. They're settings. The first sets a default environment variable. PORT is three thousand, unless you set it when you run the container. Expose documents the port the app uses. It doesn't publish it. You still do that when you run it. User switches away from root, for everything after it. And the last line is the command a container runs when it starts. Work that happens at build time belongs in a run line, not here.

[01:41] Build it

Now build it, and name the image my app. The dot at the end is the build context. That's the folder the copy lines read from. Watch the steps. There are five: the base, the folder, two copies, and the install. The four settings don't show up as steps. They're recorded in the image, for whoever runs it.

[02:03] Run it

Now run it, with a port published. The left side is any free port on your machine. The right side is three thousand, where the app listens. It says it's listening on port three thousand. That's the default from the environment line. And a request from your machine gets the greeting back. Set PORT to four thousand when you run it, and the same image listens there instead.

[02:29] Why dependencies come first

So why copy the dependency list first? Change one line of the code, and build again. Look at the install step. It says cached. Docker reused that layer, because the dependency list didn't change. Only the final copy ran again. So copy the dependency list before the source, and the install stays cached while you work on your code.

[02:54] Recap

To recap. The first line picks the base image. Copy and run add layers of files. The environment variable, the exposed port, the user and the command are settings. And dependencies go in before the source, so the install stays cached.

Here's a question to check yourself. A Dockerfile exposes port eighty eighty. You run it without publishing a port. Can you reach the app? No. Expose only records the port. Publishing it is a choice you make when you run the container. Next, multi-stage builds. I'll see you there.

Key idea

A Dockerfile is a recipe for an image, read top to bottom. Each instruction either adds a layer of files or records a setting, and docker build runs them in order.

One Dockerfile, line by line

This one packages a small Node.js web app:

# Start from an image that already has Node.js 22 installed
FROM node:22-alpine

# Every later instruction runs in /app
WORKDIR /app

# Copy the dependency list first, then install from it
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Now copy the rest of the source
COPY . .

# Settings and documentation for whoever runs the image
ENV PORT=3000
EXPOSE 3000

# Drop root, then say what to run when a container starts
USER node
CMD ["node", "server.js"]
  • FROM picks the base image: everything else is built on top of it.
  • COPY and RUN add layers. RUN executes a command at build time, here installing dependencies.
  • ENV sets a default variable; -e can override it at run time.
  • EXPOSE documents the port. It doesn't publish it: -p still does that.
  • USER switches away from root for everything after it.
  • CMD is the command a container runs. Build-time work belongs in RUN.

Dependencies are copied before the source on purpose: when only your code changes, Docker reuses the cached install layer.

Build and run it:

docker build -t my-app .
docker run -p 3000:3000 my-app

The . is the build context, the folder COPY reads from. .dockerignore and build context covers it.

Check yourself

A Dockerfile has EXPOSE 8080. You run docker run my-app with no -p. Can you reach the app at localhost:8080?