1. Tagging Conventions & Registries
An image name is really [registry-host/]repository:tag. Leave the
registry host off and Docker assumes Docker Hub; leave the tag off and Docker
assumes :latest. Neither default is something you want to rely on in a
real pipeline — :latest tells you nothing about which commit produced
the image, and omitting the registry host silently points you at the wrong place
once you start using more than one registry.
docker.io/adalovelace/orders-api:1.4.2 # Docker Hub, explicit
123456789012.dkr.ecr.us-east-1.amazonaws.com/orders-api:1.4.2 # AWS ECR
ghcr.io/adalovelace/orders-api:1.4.2 # GitHub Container Registry
A common, durable tagging scheme combines a build identifier with a mutable "floating" tag so both precision and convenience are available:
# Tag the same built image twice: once immutable, once floating
docker build -t orders-api:git-a1b2c3d .
docker tag orders-api:git-a1b2c3d orders-api:latest
# Or bake the git SHA in directly from CI
docker build -t orders-api:$(git rev-parse --short HEAD) .
Docker Hub is the default public registry and the simplest place to start — free public repositories, one private repository on the free tier. AWS ECR (Elastic Container Registry) is what you'll actually deploy from once your workloads run on AWS starting Week 8: it's private by default, integrates with IAM for access control instead of a separate login, and keeps images in the same region as the ECS or EKS workloads that pull them, which avoids cross-region transfer time and cost. Most real projects use both — Docker Hub or GHCR for anything public, ECR for anything that runs in production.
:latest
If a bad deploy uses :latest, rolling back means "whatever the previous :latest happened to be" — which nobody can reconstruct with confidence. Deploy an immutable tag like a git SHA, and reserve :latest for local development convenience only.
2. Authenticating & Pushing/Pulling Images
Pushing to Docker Hub uses a personal access token instead of your account password — Docker Hub deprecated password-based CLI login for exactly the reason you'd expect: a token can be scoped and revoked without touching the account itself.
# Generate a token at hub.docker.com > Account Settings > Security first
docker login -u adalovelace
Password: <paste access token, not your account password>
docker tag orders-api:1.4.2 adalovelace/orders-api:1.4.2
docker push adalovelace/orders-api:1.4.2
docker pull adalovelace/orders-api:1.4.2
ECR authentication works differently: there's no long-lived password at all. The
AWS CLI mints a short-lived authorization token (valid 12 hours) that you pipe
straight into docker login, which is the same mechanism your CI runner
will use in Week 6 instead of storing a registry password as a secret.
# Create the repository once
aws ecr create-repository --repository-name orders-api --region us-east-1
# Authenticate Docker against ECR using a short-lived token
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Tag with the full ECR repository URI and push
docker tag orders-api:1.4.2 \
123456789012.dkr.ecr.us-east-1.amazonaws.com/orders-api:1.4.2
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/orders-api:1.4.2
docker login writes credentials to ~/.docker/config.json
— on a shared or CI machine, prefer a credential helper
(docker-credential-ecr-login for ECR) over letting a plaintext token
sit in that file longer than the current session needs it.
An ECR auth token expiring after 12 hours means a leaked token from a compromised laptop or log file is useless the next day without any manual revocation. This is the same tradeoff you'll see again with Terraform's AWS provider and Kubernetes service account tokens — short-lived credentials minted on demand beat long-lived secrets sitting in a config file.
3. Image Vulnerability Scanning
Every base image bundles an operating system's worth of packages, and those packages accumulate published CVEs (Common Vulnerabilities and Exposures) over time. Scanning turns "we hope our base image is fine" into a specific, actionable list of known issues, ranked by severity, before that image ever reaches production. Two tools cover almost every workflow you'll meet: Docker Scout, built into the Docker CLI, and Trivy, an open-source scanner from Aqua Security that's easy to drop into any CI pipeline.
docker scout quickview orders-api:1.4.2
docker scout cves orders-api:1.4.2
# Compare against the currently deployed tag to see what a rebuild would fix
docker scout compare orders-api:1.4.2 --to orders-api:1.4.1
# Install: https://aquasecurity.github.io/trivy — or run via the Trivy image itself
trivy image orders-api:1.4.2
# Fail the command (useful as a CI gate) only on HIGH/CRITICAL findings
trivy image --severity HIGH,CRITICAL --exit-code 1 orders-api:1.4.2
# Scan the Dockerfile itself for misconfigurations, separately from the built image
trivy config .
A scan report lists each vulnerable package, its installed version, the fixed version (if one exists), and a severity rating. Most findings resolve by rebuilding against a newer base image tag rather than patching packages by hand — which is why scanning a stale image weekly, not just at build time, matters: the code hasn't changed, but new CVEs get published against the packages already baked into it every day it sits in the registry.
trivy image --exit-code 1 is designed to be dropped straight into a CI job as a pass/fail gate — that's exactly what you'll wire into the build-test-push pipeline next week, and again as one of the DevSecOps controls in Week 14.
4. Keeping Image Size Down
A smaller image pulls faster, scans faster, and carries a smaller attack surface —
fewer installed packages means fewer possible CVEs. Three habits get you most of
the way there, and you already met two of them building your Dockerfile
in Week 3.
# Stage 1: build with full tooling
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: ship only the runtime, on a slim base
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]
node:20 carries a full Debian userland and build toolchain — fine for
compiling, wasteful for running. node:20-slim (or -alpine
for an even smaller, musl-based image) strips that down to roughly a tenth of the
size. The multi-stage build lets stage 1 use the fat image for compiling while only
stage 2's output — the slim image plus your built artifacts — actually gets pushed.
The third habit is a .dockerignore file, which keeps the build
context — everything sent to the Docker daemon before a build even starts — from
including files that bloat layers or leak secrets into the image:
node_modules
.git
.env
*.log
dist
coverage
Dockerfile
.dockerignore
Check the real impact with docker images, and confirm a build didn't
silently regress with docker history, which prints the size each
instruction in the Dockerfile added:
docker images orders-api
docker history orders-api:1.4.2
Every package you don't install is a package that can never show up in a CVE scan. Treat "keep it small" and "keep it scanned clean" as one habit, not two separate chores — a slim, multi-stage image is usually most of the way to a clean scan before you even run one.
5. Hands-on Exercise
Push a scanned, slimmed image to two registries
Take last week's Compose app's main service image and put it through a real registry workflow, end to end.
Requirements:
- Rewrite the service's
Dockerfileas a multi-stage build ending on a-slimor-alpinebase image, and add a.dockerignorethat excludesnode_modules,.gitand.env. - Build it tagged with the current git short SHA:
docker build -t orders-api:$(git rev-parse --short HEAD) ., then confirm the size drop withdocker imagesagainst your Week 3 build. - Create a free Docker Hub account (if you don't have one), generate an access token, and push the image as
<your-username>/orders-api:<sha>. - Create an AWS ECR repository named
orders-api, authenticate withaws ecr get-login-password, and push the same image tagged with the full ECR repository URI. - Run
trivy image --severity HIGH,CRITICAL --exit-code 1against the pushed image and fix at least one finding by bumping the base image tag. - Write down, in a short paragraph, the size difference between your original and slimmed image and one CVE the scan found before the fix.
If you don't have an AWS account yet, create the free-tier account now — you'll need it again in Week 8 regardless, and ECR's free tier (500 MB/month private storage) is more than enough for this exercise.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is deploying off an image tagged :latest risky?
Why is deploying off an image tagged :latest risky?
:latest is a mutable, floating pointer — it can be reassigned to a different build at any time, and by the time you need to roll back you often can't reconstruct what it previously pointed to. An immutable tag like a git SHA always refers to exactly one build, so rollback means "redeploy this specific known-good tag" instead of guessing.
Q2
Why doesn't ECR use a stored username/password like Docker Hub's older login flow?
Why doesn't ECR use a stored username/password like Docker Hub's older login flow?
ECR relies on aws ecr get-login-password to mint a short-lived token (expiring in 12 hours) tied to your IAM identity, piped directly into docker login. A leaked token is only useful for a few hours and never needs manual revocation, unlike a long-lived password that stays valid until someone notices and rotates it.
Q3
What would happen if you scanned an image once at build time and never again?
What would happen if you scanned an image once at build time and never again?
New CVEs get published against already-installed package versions constantly, so an image that scanned clean on day one can become vulnerable weeks later even though nothing in it changed. A stale image sitting in a registry needs periodic rescanning, not just a one-time check at build time, to stay trustworthy.
Q4
In a multi-stage Dockerfile, why does only the final stage end up in the pushed image?
In a multi-stage Dockerfile, why does only the final stage end up in the pushed image?
Each FROM line starts a fresh, independent stage; earlier stages only persist into later ones through explicit COPY --from=<stage> instructions. So a heavy build stage's compiler and toolchain never get copied forward — only the specific build artifacts you name end up in the final image, which is what keeps it slim.