📖 Lecture — Multi-Stage Dockerfiles, Tagging, and Image Security

In earlier weeks of this course you pulled and ran containers built by other people. Now you become the person who builds them — and who ships them safely to a registry others will pull from. A Dockerfile is a simple text recipe: a list of instructions Docker executes in order to produce an image. Get the order, structure, and downstream handling right and you get fast builds, small images, and a safe production footprint. Get it wrong and you get bloated multi-gigabyte images, slow rebuilds, and avoidable security holes.

Layer caching and instruction order. Every instruction (FROM, RUN, COPY, etc.) creates a cached layer. On rebuild, Docker reuses cached layers until it hits the first changed instruction — everything after that rebuilds from scratch. Copy your entire app source before pip install and every code tweak forces a full dependency reinstall. The fix: copy only the dependency manifest (requirements.txt, package.json) first, install dependencies, then copy application code last. Since code changes far more often than dependencies, routine edits only rebuild the cheap final layer.

Multi-stage builds. A single-stage Dockerfile installs compilers, build tools, and every dependency into one image — and all of it ships in your final image even though you only needed it to build, not run, the app. Multi-stage builds use more than one FROM in the same Dockerfile: an early "builder" stage installs heavyweight tools and compiles or installs everything needed; a final stage starts fresh from a slim base image and uses COPY --from=<builder stage> to pull across only the runtime artifacts it actually needs. This routinely shrinks image size by 60–90% and shrinks the attack surface — fewer packages means fewer things that can carry a vulnerability.

Non-root users, and one RUN for apt-get. A container runs as root by default. If a root process is compromised, the attacker has root inside the container — a much bigger blast radius than an unprivileged user. Create a dedicated user (RUN useradd -m appuser), switch with USER appuser, and set ownership on copy with COPY --chown=appuser:appuser . .. Separately, splitting RUN apt-get update from RUN apt-get install across two cached layers risks a stale, silently outdated package index at install time — combine them: RUN apt-get update && apt-get install -y <pkg> && rm -rf /var/lib/apt/lists/*.

From build to registry: tagging. A container registry (Docker Hub being the most widely used) stores, versions, and serves images. The workflow is three commands: docker login, docker tag <image> <username>/<repo>:<tag>, docker push <username>/<repo>:<tag> — tags are just labels pointing at layers, so one build can carry several tags. Semantic versioning (MAJOR.MINOR.PATCH) communicates intent, and once a version tag like v1.0.1 is pushed it should be treated as immutable — never overwritten. latest is mutable by convention: it simply points at whatever was most recently pushed untagged, with no guarantee it's the newest, best-tested, or safest build.

| Tag | Mutable? | Safe for production? | Why | |---|---|---|---| | latest | Yes | No | Points to whatever was last pushed untagged; can silently change under you | | v1.0.1 | Should be treated as no | Yes | New patch gets its own tag, not a rewrite of the old one | | sha256:<digest> | No (cryptographically fixed) | Yes (strongest) | Pins to exact content; immune to tag reuse or tampering |

Scanning: don't trust, verify. Pulling an image doesn't mean it's safe — independent analyses have repeatedly found malicious images on Docker Hub, including cryptomining trojans under typosquatted names. Trivy (Aqua Security) is a free, single-binary scanner that checks images, Dockerfiles, and Kubernetes manifests for known CVEs. Grype (Anchore) is a strong alternative — faster pure-vulnerability scans, plus SBOM (Software Bill of Materials) generation. In March 2026, the Trivy project itself was compromised: images tagged 0.69.40.69.6, and latest during the window, exfiltrated credentials from anyone who pulled them. The lesson: pin to a known-good digest and verify provenance before trusting any image — including your security tooling. In CI, don't just scan once manually — gate the pipeline with a severity threshold (fail on CRITICAL/HIGH) combined with --ignore-unfixed, so a vulnerable image structurally cannot reach production.

Correcting three common misconceptions:

  1. "I can put a password or API key in a Dockerfile as long as I don't share it." False. Every instruction becomes a baked-in layer — even if a later RUN deletes the secret, the earlier layer containing it still exists and can be extracted. Inject secrets at runtime (env vars, mounted files, a secrets manager) — never via ENV, ARG, or hardcoded strings.
  2. "Instruction order is just for readability." False — order determines cache efficiency. Put what changes least often (base image, system packages) near the top, and what changes most often (app source) at the bottom.
  3. "latest means newest and best-tested, and a public registry means a safe registry." Both false. latest is only a label for the most recent untagged push, and public registries host millions of images from anyone — vulnerable or malicious images are a documented, ongoing problem. Scanning and provenance checks are required steps, not optional extras.

Master these habits and your images will build faster, ship smaller, and be measurably harder to exploit or spoof — exactly what you want before an AI workload reaches production.