A Dockerfile is a recipe that builds an image, one instruction at a time. Each instruction creates a layer, and Docker caches layers, so the order of instructions decides whether a rebuild takes two seconds or two minutes. This post builds a small Node.js API image the way you would for production.
Versions in use on this date. Docker Engine 19.03.9, Node.js 12 (Active LTS; Node 14 is the current release and becomes LTS in October). node:12-alpine is based on Alpine Linux and is much smaller than the Debian-based image, with the caveat that Alpine uses musl instead of glibc, which some native modules do not like.
Build flow and caching
flowchart TB
CTX[Build context<br/>files minus .dockerignore] --> F[FROM node 12-alpine]
F --> W[WORKDIR /app]
W --> P[COPY package files]
P --> I[RUN npm ci --only=production<br/>cached until package files change]
I --> S[COPY source code<br/>changes often]
S --> U[USER node]
U --> E[CMD node server.js]
E --> IMG[Image tagged myapi 1.0.0]
P -.->|unchanged| CA[Cache hit and skip install]
Put things that change rarely (dependency install) before things that change often (your source). Then editing a source file does not reinstall every dependency.
The application
// server.js
const http = require('http');
const port = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
if (req.url === '/healthz') { res.writeHead(200); return res.end('ok'); }
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'hello', version: process.env.APP_VERSION || 'dev' }));
});
server.listen(port, () => console.log(`listening on ${port}`));
process.on('SIGTERM', () => server.close(() => process.exit(0)));
The SIGTERM handler lets docker stop shut the process down cleanly instead of waiting 10 seconds for a kill.
The Dockerfile
FROM node:12-alpine
ENV NODE_ENV=production
WORKDIR /app
# dependencies first, to use the build cache
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# then the source
COPY . .
# do not run as root
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/healthz || exit 1
CMD ["node", "server.js"]
.dockerignore keeps junk out of the build context (faster builds, no leaked secrets):
node_modules
.git
.env
*.log
Dockerfile
npm-debug.log
Build and run
docker build -t myapi:1.0.0 .
docker images myapi
docker run -d --name myapi -p 3000:3000 -e APP_VERSION=1.0.0 myapi:1.0.0
curl localhost:3000
docker inspect --format '{{.State.Health.Status}}' myapi
docker history myapi:1.0.0 # layers and sizes
Rebuild after editing server.js: the npm ci layer shows Using cache, and only the COPY . . layer and later ones are re-run.
Instruction cheat sheet
| Instruction | Purpose |
|---|---|
FROM |
Base image; pin a version tag |
WORKDIR |
Set (and create) the working directory |
COPY / ADD |
Copy files in; prefer COPY (ADD also fetches URLs and unpacks archives) |
RUN |
Execute during build and bake the result into a layer |
ENV / ARG |
Runtime variable / build-time variable |
EXPOSE |
Documentation of the port; does not publish it |
USER |
Drop root for the process |
ENTRYPOINT / CMD |
The program, and its default arguments; use the JSON (exec) form so signals reach your app |
HEALTHCHECK |
Tells Docker how to test the container |
Habits that matter
- Never put secrets in the image (
ENV PASSWORD=..., copied.env): they remain in layers and indocker history. Pass them at runtime or use a secret store. - Pin base image versions (
node:12.18-alpine), and rebuild regularly to receive security patches. - Combine related commands in one
RUNand clean up in the same layer (rm -rf /var/cache/apk/*), because deleting a file in a later layer does not shrink the image. - One process per container, logging to stdout.
- Keep images small: fewer packages mean fewer vulnerabilities (scanning is covered in a later post).
- Tag with versions or Git commit hashes, not just
latest. - Multi-stage builds, which separate build tools from the runtime image, are the next step; see the multi-stage post later in this series.