This lesson examines the instructions that shape an image's filesystem, one at a time, with the options and pitfalls that separate a working Dockerfile from a good one. After it you will choose the right base image syntax, write RUN steps that cache well, know when ADD is justified over COPY, and understand what EXPOSE does and does not do.
FROM must be the first instruction (only ARG and the # syntax directive may precede it). It sets the base image and starts a new build stage:
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
FROM --platform=$BUILDPLATFORM golang:1.24 AS builder
FROM scratchnode:22), or a full tag (node:22.12-alpine3.21) for reproducibility; latest changes underneath you.AS name labels a stage so later stages can reference it with COPY --from=name.scratch is an empty image, used for statically linked binaries.# syntax=docker/dockerfile:1 line asks BuildKit for the latest stable Dockerfile frontend, enabling features such as heredocs and mount flags.RUN executes a command in a new layer and commits the result. Combine related commands so they share one layer and clean up in the same step:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*Because each RUN is a layer, deleting files in a later RUN does not shrink the image; the bytes remain in the earlier layer. With BuildKit you can also write multi-line scripts as heredocs, which is easier to read than chained &&:
RUN <<EOF
set -e
apk add --no-cache curl
adduser -D -u 1001 appuser
EOFRUN has an exec form too (RUN ["sh", "-c", "..."]), but the shell form is the norm for build steps. Mount flags such as RUN --mount=type=cache,target=/root/.npm npm ci keep package caches out of the image, a technique the layer-caching lesson covers.
Both copy files from the build context into the image, but ADD has two extra behaviours that surprise people: it auto-extracts local .tar, .tar.gz and similar archives, and it can download from a URL.
| Feature | COPY | ADD |
|---------|--------|-------|
| Copy files and directories from context | Yes | Yes |
| Extract local tar archives automatically | No | Yes |
| Fetch a remote URL | No | Yes (not cached, no checksum by default) |
| --from=stage for multi-stage builds | Yes | No |
| --chown, --chmod | Yes | Yes |
| --link (layer independent of earlier layers) | Yes | Yes |
The rule is simple: use COPY unless you specifically need tar extraction. For URLs, RUN curl gives you control over checksum verification and cleanup.
COPY package.json package-lock.json ./
COPY --chown=node:node --chmod=755 scripts/entrypoint.sh /usr/local/bin/
COPY --from=builder /app/dist ./dist
ADD vendor/lib.tar.gz /opt/lib/ # extracted into /opt/libTwo details about paths: sources are relative to the build context and cannot reach outside it (../secrets fails), and a destination ending in / is treated as a directory. Trailing-slash mistakes are the most common reason a file ends up named dist instead of inside dist/.
WORKDIR sets the directory for every following RUN, CMD, ENTRYPOINT, COPY and ADD, creating it if needed. Prefer it over RUN cd, which only affects that single layer:
WORKDIR /app # absolute path; created automatically
COPY . . # copies into /app
WORKDIR src # relative: now /app/srcAlways use absolute paths for the first WORKDIR; relative ones resolve against whatever the base image set, which may not be /.
EXPOSE 3000 records that the container listens on port 3000. It does not publish the port. Publishing still requires -p 8080:3000 at run time (or ports: in Compose). What EXPOSE gives you:
docker inspect and image registries.docker run -P (capital P) publishes every exposed port to a random high host port.EXPOSE 3000
EXPOSE 9229/tcp 5000/udpLABEL org.opencontainers.image.source="..." attaches metadata; registries such as GHCR read the OCI labels.VOLUME /data declares an anonymous volume mount point; use it sparingly because later RUN changes to that path are discarded.STOPSIGNAL SIGQUIT changes the signal docker stop sends; nginx images set this for graceful shutdown.Which instruction should you use to copy a local `.tar.gz` and have it extracted automatically?
FROM starts a stage; pin versions and name stages with AS for multi-stage builds.RUN commands and clean up in the same layer; heredocs keep long steps readable.COPY by default; reserve ADD for local tar extraction and avoid its URL feature.WORKDIR replaces RUN cd and applies to all subsequent instructions.EXPOSE documents ports and enables -P; it never publishes anything on its own.Next lesson: CMD vs ENTRYPOINT: Shell Form, Exec Form and Overrides — define what your container runs and how it receives arguments and signals.