Docker and Kubernetes course menu
How to Dockerize a Node.js and Express App
Write a good Dockerfile for a Node.js app: an Alpine base image, npm ci, production dependencies only, the built-in node user, and handling shutdown signals.
Now we package an Express app.
The app#
mkdir node-app
cd node-app
cat > package.json <<'EOF'
{
"name": "node-demo",
"version": "1.0.0",
"type": "module",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^5.0.0" }
}
EOF
cat > server.js <<'EOF'
import express from "express";
const app = express();
const port = process.env.PORT ?? 3000;
app.get("/", (req, res) => res.json({ message: "Hello from Express" }));
app.get("/health", (req, res) => res.json({ status: "ok" }));
const server = app.listen(port, () => console.log(`Listening on ${port}`));
process.on("SIGTERM", () => {
console.log("Shutting down");
server.close(() => process.exit(0));
});
EOF
cat > .dockerignore <<'EOF'
node_modules
npm-debug.log
.env
.git
EOF
docker run --rm -v "$PWD":/app -w /app node:24-alpine npm install --package-lock-only --silent 2> /dev/null
lspackage-lock.json
package.json
server.jsWe used a container to create package-lock.json, so Node.js did not need to be installed. You should commit that file.
The Dockerfile#
cat > Dockerfile <<'EOF'
FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
EOFnpm ciinstalls the exact versions from the lock file, and it is the right command for builds.--omit=devleaves out development tools.node_modulesis in.dockerignore, so that packages from your computer never overwrite those installed for Linux.- The official image includes a user called
node. CMD ["node", "server.js"]runs Node directly.
Build and run#
docker build -q -t node-demo:1.0 . > /dev/null
docker run -d --name api -p 3000:3000 node-demo:1.0 > /dev/null
sleep 2
curl -s http://localhost:3000/
echo
docker exec api whoami
docker images node-demo --format '{{.Size}}'{"message":"Hello from Express"}
node
177MBClean shutdown#
docker stop sends SIGTERM. Our code catches it, finishes current requests, and exits:
time docker stop api > /dev/null
docker logs api
docker rm api > /dev/null
real 0m0.149s
user 0m0.006s
sys 0m0.009s
Listening on 3000
Shutting downIt stopped almost at once.
If a program does not handle signals, docker run --init adds a small helper process to pass them on.
Common mistakes#
Practice#
Practice exercise
Run the image on host port 3100 with PORT=4000 set inside the container, and call /health.
Show hint
The container now listens on 4000, so use -p 3100:4000.
Show solution
docker run -d --name p1 -p 3100:4000 -e PORT=4000 node-demo:1.0 > /dev/null
sleep 2
curl -s http://localhost:3100/health
echo
docker rm -f p1 > /dev/null{"status":"ok"}Key takeaway#
Next: Multi-Stage Builds.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.