Expert · Deployment and scaling · topic 48 of 54 · 30 min
Docker: packaging your app
Explanation
The "works on my machine" problem
Your API works on your laptop. On the server it crashes, because the server has an older Node.js, a missing library or a different setting. Docker fixes this by shipping the app with its environment. If the container runs on your laptop, the same container runs on the server.
Image and container
An image is a read-only package: a small Linux system, the runtime (for example Node.js), your code and its dependencies. A container is a running copy of an image, with its own processes and network. You can start many containers from one image, and stop and throw them away at any time.
A Dockerfile for a Node.js API
# Stage 1: build
FROM node:24-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: run
FROM node:24-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]- FROM picks the base image. node:24-slim is a small Debian image with a Node.js LTS version. Always pin a version; :latest can change under you.
- COPY package*.json and RUN npm ci come BEFORE COPY . . so Docker can reuse the installed packages (its layer cache) when only your code changes. Builds go from minutes to seconds.
- Multi-stage: the first stage compiles; the second copies only the result. The final image is smaller and has fewer tools an attacker could use.
- USER node runs the app as a normal user, not root, so a bug in your app cannot take over the whole container as easily.
- CMD is the command that runs when the container starts.
# .dockerignore: files Docker must NOT copy into the image
node_modules
.git
.env
dist
*.logBuild, run and inspect
docker build -t notes-api:1.4.0 . # build an image from the Dockerfile here
docker run -d -p 8080:3000 --env-file .env --name notes notes-api:1.4.0
docker ps # list running containers
docker logs -f notes # follow the container's logs
docker stop notes && docker rm notes # stop and remove itPorts: a container has its own network, so your app listening on port 3000 inside it is not reachable until you publish the port with -p host:container. Volumes: a container's files disappear when it is removed. A volume is storage that lives outside the container, so database files survive restarts and upgrades.
Docker Compose: app + Postgres + Redis
# compose.yaml (local development only)
services:
api:
build: .
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://app:devpass@db:5432/notes
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpass
POSTGRES_DB: notes
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7
volumes:
pgdata:A registry is a store for images, like an app store for containers. Docker Hub, GitHub Container Registry (ghcr.io), AWS ECR and Google Artifact Registry are common. Your CI builds the image once, pushes it with a version tag, and every server pulls exactly that image.
Watch the lesson
Docker: packaging your app · 1/7
It works on my machine is a famous problem. Docker fixes it by shipping your app together with its environment.
Prefer a video by a teacher? Find free videos on “docker tutorial for beginners node js dockerfile compose” on YouTube ↗
In real life: A new developer joins a team in Bengaluru
Before Docker, a new developer spent two days installing the right Postgres, Redis and Node.js versions, and still had a bug that "only happens on Priya's laptop". Now the project has a compose.yaml. The new developer clones the repo, runs docker compose up, and the API, a Postgres 17 database and Redis are running in five minutes, with the same versions as production. The CI builds the same Dockerfile, so what was tested is exactly what is deployed.
Practice (0/3 done)
docker run -p takes a mapping like "8080:3000" (host port : container port). Write parsePortMapping(text) that returns { host: 8080, container: 3000 } as numbers. If the text does not have exactly two parts, or either part is not a whole number from 1 to 65535, throw an Error.
Write lintDockerfile(text) that returns a list of warnings, in this order. 1) For every FROM line whose image has no tag or the tag latest, add "line N: pin a version instead of latest (IMAGE)". 2) If some RUN line contains "npm ci" or "npm install", and a "COPY . <dest>" line (source is exactly ".") comes before the first such RUN line, add "line N: COPY . comes before npm install" for that COPY line. 3) If there is no USER line at all, add "no USER line: the container will run as root" at the end. N is the 1-based line number. Warnings 1 and 2 go in line order.
Write a compose.yaml for local development of a Notes API with Postgres and Redis, plus a .dockerignore. Start it with docker compose up -d, check docker ps shows three containers, then stop it with docker compose down.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.