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:
npx create-next-app@latest my-next-app
cd my-next-appAccept 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
FROM node:20-alpineEvery 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
WORKDIR /appThis 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
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
RUN npm installRUN 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
COPY . .Now copy everything else: your source code, next.config.mjs, and so on.
RUN — build the app
RUN npm run buildThis produces the compiled, production-ready version of your Next.js app.
EXPOSE — label the port
EXPOSE 3000Next.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
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
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
.jsxpage), Docker reuses the cachednpm installlayer instead of reinstalling every package. - If you had written
COPY . .beforeRUN 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:
docker initQuick Check
- Can you explain what each instruction in the Dockerfile does, without looking?
- Can you explain why
package.jsonis copied before the rest of the code? - Do you know what
EXPOSEdoes and does not do?
Next → Build, Run & Push