Skip to Content
DocsDockerBuilding ImagesWriting a Dockerfile

Writing a Dockerfile

This page walks through a Dockerfile for a Next.js app, line by line. By the end, you will understand every instruction — not just be able to copy one.

What a Dockerfile Is

Recall from Foundation: an image is a blueprint, and a container is a running instance of it. A Dockerfile is the recipe that produces that blueprint.

It is a plain text file listing instructions. Docker reads it from top to bottom and builds an image, one instruction at a time.

Before You Start

You need a Next.js app to package. If you already have one, use it. Otherwise, create one quickly:

Terminal
npx create-next-app@latest my-next-app cd my-next-app

Accept the defaults if asked — anything it creates will work for this page.

Writing the Dockerfile

Create a new file named exactly Dockerfile (no file extension) in the root of your project, next to package.json.

FROM — pick a base image

Dockerfile
FROM node:20-alpine

Every image starts from another image — you never build completely from scratch. This line starts from an official Node.js image that already has Node.js and npm installed.

alpine refers to Alpine Linux, a minimal Linux distribution. Using it as a base keeps the image small.

WORKDIR — set the working folder

Dockerfile
WORKDIR /app

This creates a folder called /app inside the image and moves into it. Every instruction after this one runs from /app.

COPY — bring in dependency files first

Dockerfile
COPY package.json package-lock.json ./

This copies only two files — not your whole project yet. This is intentional, and the reason is explained below in “Why the Order Matters.”

RUN — install dependencies

Dockerfile
RUN npm install

RUN executes a command while the image is being built. This one installs every package listed in package.json.

COPY — bring in the rest of the app

Dockerfile
COPY . .

Now copy everything else: your source code, next.config.mjs, and so on.

RUN — build the app

Dockerfile
RUN npm run build

This produces the compiled, production-ready version of your Next.js app.

EXPOSE — label the port

Dockerfile
EXPOSE 3000

Next.js listens on port 3000 inside the container. EXPOSE does not connect that port to your machine — it is just a note for anyone reading the Dockerfile, saying “this container uses port 3000.”

The actual connection is made later, with -p, when you run the container. Without -p, the app is still running inside the container, but your browser cannot reach it — there is no path yet from your machine into the container. -p 3000:3000 builds that path.

CMD — the startup command

Dockerfile
CMD ["npm", "start"]

This is the command that runs when a container starts from this image. It must be the last instruction.

The Full File

Dockerfile
FROM node:20-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm install COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "start"]

This Dockerfile works, but it is not yet the smallest or safest version. The next two pages improve on it — first by testing it, then by shrinking it. Do not skip ahead; each step builds on the last.

Why the Order Matters

Docker saves the result of each instruction as a layer, and reuses layers it has already built if nothing changed — this is called the build cache.

This is exactly why the Dockerfile copies package.json and package-lock.json before copying the rest of the code:

  • If you only change a source file (like a .jsx page), Docker reuses the cached npm install layer instead of reinstalling every package.
  • If you had written COPY . . before RUN npm install, any source code change — even a one-line typo fix — would invalidate the cache and force a full reinstall.

This one ordering trick is one of the most common ways real projects speed up their Docker builds.

A Shortcut for Later: docker init

Docker has a command, docker init, that asks a few questions and generates a Dockerfile, a .dockerignore file, and a Compose file for you automatically.

It is genuinely useful — but this page had you write one by hand first on purpose. Once you understand what each line does, docker init’s output stops being a mystery, and you can read and adjust what it generates. Try it on your next project:

Terminal
docker init

Quick Check

  • Can you explain what each instruction in the Dockerfile does, without looking?
  • Can you explain why package.json is copied before the rest of the code?
  • Do you know what EXPOSE does and does not do?

Next → Build, Run & Push

dockerfile nextjs, how to write a dockerfile, docker layers cache explained, dockerfile instructions

Last updated on