Writing Your First Dockerfile for CI
A good Dockerfile is short, deterministic, and orders its instructions to maximize cache hits.
Now you will write a real Dockerfile suited for continuous integration. The goal is a build that is fast, reproducible, and cache-friendly. We will build it up instruction by instruction and explain why each line is ordered the way it is.
Choose a small, pinned base image
Start FROM a specific, slim base image -- never latest. Pinning a tag like node:20-slim keeps builds reproducible, and slim or alpine variants shrink download and build time on every CI run.
A complete first Dockerfile
This Dockerfile installs dependencies before copying source so the dependency layer stays cached when only code changes.
FROM node:20-slim
WORKDIR /app
# Copy only manifests first so this layer caches across code changes
COPY package*.json ./
RUN npm ci
# Now copy the rest of the source
COPY . .
RUN npm run build
CMD ["npm", "start"]Why instruction order matters
Docker caches each layer and invalidates everything after the first changed instruction. By copying package*.json and running npm ci before COPY . ., a code-only change reuses the cached dependency layer. Reversing the order would reinstall every dependency on every commit.
Run your tests against the image
Build the image and run your test suite inside a throwaway container. This is the core of a containerized CI job.
docker build -t myapp:ci .
docker run --rm myapp:ci npm testKey takeaways
- Pin a small base image (like node:20-slim) instead of using latest.
- Copy dependency manifests and install before copying source to maximize layer cache hits.
- Build the image, then run tests inside an ephemeral container with --rm.