After this lesson, you will be able to: Write a Dockerfile from scratch: FROM, RUN, COPY, WORKDIR, ENV, EXPOSE, CMD vs ENTRYPOINT. Build and tag images.
A Dockerfile is a script that produces an image. Master the syntax and you can package any app.
This is the shape; we'll iterate to production-grade in later lessons.
# syntax=docker/dockerfile:1FROM node:20-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --omit=devCOPY . .EXPOSE 3000CMD ["node", "server.js"]# Build: docker build -t myapp:1.0 .# Run: docker run -p 3000:3000 myapp:1.0
FROM <image>: base image. Every Dockerfile starts here. WORKDIR <path>: cd into this directory for subsequent commands (creates it if missing). COPY <src> <dest>: copy from host build context into the image. RUN <cmd>: execute at build time; the result becomes a layer. ENV KEY=val: set environment variable in the resulting image. EXPOSE 3000: documents the port (does NOT actually publish it; `-p` does that). CMD ["node", "server.js"]: default command when the container starts (overridable). ENTRYPOINT ["node"]: fixed entry; arguments after `docker run image` become CMD.
CMD alone: the default command, overridable. `docker run myapp echo hi` runs echo, not your app. ENTRYPOINT alone: a fixed prefix. `docker run myapp arg1 arg2` runs ENTRYPOINT + arg1 arg2. Both together: ENTRYPOINT is the binary; CMD is the default args. The cleanest pattern: ENTRYPOINT ["node"], CMD ["server.js"]. Use the JSON-array form (exec form): CMD ["node", "server.js"]. The string form invokes a shell, which complicates signal handling.
The full build → tag → run loop.
# Build (read Dockerfile in current dir, build context = .)docker build -t myapp:1.0 .# Tag for a registrydocker tag myapp:1.0 ghcr.io/myorg/myapp:1.0# List built imagesdocker images# Run, override CMDdocker run --rm myapp:1.0 node --version# Pass build argsFROM node:${NODE_VERSION:-20}-alpine # in Dockerfiledocker build --build-arg NODE_VERSION=22 -t myapp:1.0 .
COPY . . at the top of the file. Invalidates the cache on every code change; rebuilds dependencies. Forgetting .dockerignore. Without it, your build context includes node_modules, .git, .env. Big, slow, leaks secrets. Using `:latest` tag in production. Reproducibility lost; rollbacks hard. Always pin: `node:20.11-alpine`. RUN with multiple lines (RUN cmd1 + RUN cmd2 + ...). Each RUN is a layer. Combine related steps: `RUN cmd1 && cmd2`. Running containers as root by default. Add `USER node` (or a non-root user) before CMD.
Pick the production reason.
Sign in and purchase access to unlock this lesson.