“It works on my machine” is the oldest joke in software, and containers are the punchline that finally landed. If you have a Node.js app and you want it to run the same way everywhere, learning to dockerize a Node.js app is one of the highest-leverage skills you can pick up. Here is a practical, no-fluff walkthrough.
The Dockerfile, line by line
A Dockerfile is a recipe for building an image. For a typical Node app, a solid starting point looks like this:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Each line earns its place. node:20-alpine is a small, current base image. WORKDIR sets the working directory. Then — and this is the important bit — we copy package*.json and install before copying the rest of the code.
Why copy package.json first?
Docker caches each layer. If you copy everything at once, changing a single line of source invalidates the cache and reinstalls all your dependencies on every build. By copying just the manifest and running npm ci first, Docker reuses the cached dependency layer whenever your dependencies have not changed. Your builds go from minutes to seconds. This one ordering trick is the difference between a Dockerfile that annoys you and one that does not.
Keep the image lean with .dockerignore
Add a .dockerignore so you do not bloat the image or bust the cache with junk:
node_modules
npm-debug.log
.git
.env
Dockerfile
Copying your host node_modules into the image is a classic mistake — it may contain platform-specific binaries that break inside the container. Let the container build its own.
Build and run
# Build the image
docker build -t my-node-app .
# Run it, mapping the port
docker run -p 3000:3000 my-node-app
Your app is now reachable at localhost:3000, running in an environment identical to what you will ship to production.
One step further: multi-stage builds
If your app has a build step (TypeScript, a bundler), use a multi-stage build: compile in one stage, then copy only the built output into a clean final image. You ship the result without the toolchain, and your image shrinks dramatically. It is the natural next move once the basics feel comfortable.
That is the whole core loop. Once you can dockerize a Node.js app confidently, deploying to virtually any cloud becomes a matter of “run this image” — and the machine-specific gremlins that used to eat your afternoons simply stop showing up.

