Exercises
Objectives
By the end of this lesson, you should be able to:
- Containerize a complete Node.js API with a multi-stage Dockerfile
- Run it alongside a real database with Docker Compose
- Confirm the containerized application behaves identically to running it directly with
node
⚠️ A note on verification: as throughout this module, Docker can’t run inside this course’s own sandboxed tooling, so the commands and expected behavior below reflect Docker’s stable, current, documented behavior rather than this course’s own live execution. This is a genuinely hands-on exercise, run every step yourself, on your own machine, with Docker installed.
Exercise: Containerizing the Notes API
a) Write a multi-stage Dockerfile for the Notes API from Authentication, Security & Testing for Node.js, following Lesson 2’s pattern:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app ./
EXPOSE 3000
CMD ["node", "app.js"]
b) Write a .dockerignore, excluding node_modules, .env*, .git, and test files.
c) Write a Compose file, the API plus a PostgreSQL database, following Lesson 3’s pattern, with the API’s DATABASE_URL pointing at the db service by name, not localhost.
d) Build and run the stack:
docker compose up --build
e) Confirm it behaves identically to running it directly. Register a user and log in, exactly as in Course 3’s integration tests, against the containerized API (http://localhost:3000), and confirm the responses match what running it directly with node app.js would produce, same status codes, same response bodies, the container changes nothing about the application’s actual behavior.
f) Confirm image size matters. Compare docker images output for a single-stage build of the same Dockerfile against the multi-stage version, and note the difference.
g) Reflect. In your own words, explain what would need to change in the Notes API’s own code, if anything, to run correctly inside this container, versus what only needed to change in how it’s built and started.
Recap
This module took a working Node.js application and gave it a genuinely portable, reproducible build: a multi-stage Dockerfile keeping the production image lean, a .dockerignore keeping secrets and unnecessary files out of it entirely, and Docker Compose coordinating the API together with a real database locally, with one command. The application’s own code, its routes, its validation, its authentication, needed no changes at all, exactly the property containers are meant to provide.
Next module: CI/CD, automatically testing and building this container on every single code change, instead of doing it by hand.