DevOps · 35 Days · Week 4 Day 19 — Container Registries
1 / 22
Week 4 · Day 19

Container Registries

Docker Hub, GHCR, ACR, ECR. Image tagging strategies that prevent deployment disasters. Trivy vulnerability scanning in CI. Image signing with cosign. Retention policies to control storage costs.

⏱ Duration 60 min
📖 Theory 25 min
🔧 Lab 30 min
❓ Quiz 5 min
❌ latest: mutable · unpredictable · unsafe ✅ v1.2.3-sha-abc1234: immutable · traceable · safe
Session Overview

What we cover today

01
Container Registries — what they are
Docker Hub, GHCR, ACR, ECR, GCR. The npm registry for container images. When to use which.
02
Image Tagging Strategy
Why :latest is dangerous. SemVer, Git SHA, combined tags. Immutable tags for reproducible deployments.
03
Trivy — deep vulnerability scanning
Scan OS packages, language libraries, binaries. Fail CI on CRITICAL. Table output, JSON for automation.
04
Registry Security
Pull secrets, image signing with cosign, retention policies, private vs public registries.
05
ACR — Azure Container Registry
ACR Tasks, geo-replication, built-in Defender scanning.
06
Full CI pipeline — build→scan→push
GitHub Actions: build → Trivy scan (fail on CRITICAL) → push to GHCR with SemVer + SHA tags.
07
🔧 Lab — GHCR push + Trivy scan
PAT auth → multi-tag build → push → Trivy scan locally → add scan to CI pipeline.
Part 1 of 5

Container Registries — npm for Docker images

Docker Hub
hub.docker.com · The default registry. All official images (nginx, postgres, node) live here. Free public repos. Free tier: 1 private repo, 100 pulls/6h unauthenticated.
docker pull nginx:alpine → pulling from Docker Hub.
GHCR — GitHub Container Registry ⭐
ghcr.io · Free. Unlimited private repos. GITHUB_TOKEN auto-auth in Actions (no secret setup). Linked to GitHub repo packages tab. Best for open-source and GitHub-hosted teams.
ghcr.io/username/myapp:v1.0.0
ACR — Azure Container Registry ⭐
azurecr.io · Managed Azure service. Best for AKS workloads — same region, no egress costs. Built-in Defender for Containers scanning. Geo-replication. Managed identity auth (no passwords).
acrinfra.azurecr.io/myapp:v1.0.0
ECR — AWS Elastic Container Registry
amazonaws.com/ecr · Best for EKS. IAM-based auth. Built-in vulnerability scanning. Lifecycle policies.
123456789.dkr.ecr.us-east-1.amazonaws.com/myapp
Registries comparison
RegistryFree?Best forAuth in CI
Docker HubLimitedPublic imagesSecret
GHCR✅ YesGitHub reposGITHUB_TOKEN
ACRPaidAzure/AKSManaged ID
ECR500MB freeAWS/EKSIAM role
The npm analogy
A registry is to Docker images what npm is to JavaScript packages.

npm install express → pulls from npm registry
docker pull nginx → pulls from Docker Hub

You push your own packages/images to share. Others pull. The registry is just a versioned storage system for these artifacts.
Part 2 of 5 — Critical

Image Tagging Strategy — latest is a lie

❌ THE :latest PROBLEM
# Monday morning: 3 AKS nodes pull image
kubectl set image deployment/app app=myapp:latest

# Node 1: already has latest cached (from Friday)
# Node 2: pulls latest (NEW version, deployed this morning)
# Node 3: pulls latest (NEW version)
#
# → Node 1 running v1.2.0
# → Node 2 running v1.3.0
# → Node 3 running v1.3.0
#
# RESULT: 3 nodes, 2 different versions
# → Inconsistent behaviour, hard to debug
# → "But it works on my machine!"

# Rollback:
# → to which 'latest'? There are two!
# → you can't reliably roll back :latest
✅ The fix — immutable tags
kubectl set image deployment/app \
  app=myapp:sha-a3f7c2d

# Every node pulls SHA sha-a3f7c2d
# sha-a3f7c2d ALWAYS points to the same image
# All 3 nodes: identical version

# Rollback:
# → deploy sha-b2e6d1c (previous SHA)
# → always reproducible
4 tagging strategies
:latestMutable. Avoid in production. OK for local dev only.
:v1.2.3SemVer. Explicit, immutable, human-readable. Tag on releases.
:sha-a3f7c2dGit SHA. Every commit gets unique tag. Perfect traceability.
:v1.2.3-sha-a3f7c2d ⭐Combined. Human-readable + traceable. Best of both worlds.
Multi-tag build script
IMAGE="ghcr.io/user/myapp"
VERSION="1.2.3"
SHA=$(git rev-parse --short HEAD)

# Build with all tags at once
docker build \
  -t $IMAGE:$VERSION \
  -t $IMAGE:$VERSION-$SHA \
  -t $IMAGE:latest \
  .

# Push all tags
docker push $IMAGE:$VERSION
docker push $IMAGE:$VERSION-$SHA
docker push $IMAGE:latest

# Kubernetes deployment uses SHA:
# image: ghcr.io/user/myapp:sha-a3f7c2d
Part 3 of 5

Trivy — deep image vulnerability scanning

Trivy commands
# Install Trivy
brew install aquasecurity/trivy/trivy    # Mac
apt-get install trivy                    # Ubuntu/Debian
trivy --version

# === Basic scan — see ALL vulnerabilities ===
trivy image myapp:v1.0.0

# Example output:
# Total: 27 (CRITICAL: 0, HIGH: 2, MEDIUM: 8, LOW: 17)
#
# ┌──────────────┬────────────────┬──────────┬────────────┐
# │  Library     │ Vulnerability  │ Severity │  Fixed In  │
# ├──────────────┼────────────────┼──────────┼────────────┤
# │ openssl      │ CVE-2023-5678  │  HIGH    │  3.1.5     │
# │ libssl       │ CVE-2023-5679  │  HIGH    │  3.1.5     │
# │ zlib         │ CVE-2023-1234  │  MEDIUM  │  1.2.13    │
# └──────────────┴────────────────┴──────────┴────────────┘

# === Filter by severity ===
trivy image --severity HIGH,CRITICAL myapp:v1.0.0

# === Exit code 1 if found (for CI gate) ===
trivy image --exit-code 1 \
            --severity HIGH,CRITICAL \
            myapp:v1.0.0
# Exit 0 → no HIGH/CRITICAL → CI passes
# Exit 1 → found HIGH/CRITICAL → CI fails

# === JSON output for automation ===
trivy image --format json \
            --output results.json \
            myapp:v1.0.0

# === Scan a remote image (from GHCR) ===
trivy image ghcr.io/user/myapp:v1.0.0

# === Scan filesystem (CI — before build) ===
trivy fs --severity HIGH,CRITICAL .

# === Scan IaC files (Terraform, K8s YAML) ===
trivy config ./k8s/
What Trivy scans
  • OS packages — Alpine apk, Debian dpkg, Ubuntu apt, RHEL rpm
  • Language libraries — Node.js (package-lock.json), Python (requirements.txt), Java (pom.xml), Go modules
  • Binaries — detects vulnerable binaries even without package metadata
  • IaC files — Terraform, Kubernetes YAML, Dockerfile, Helm charts
  • Secrets — AWS keys, API tokens, passwords in code
Trivy vs docker scout vs snyk
Trivy — open-source, free, fast, great for CI. Scans OS + app deps + IaC.
docker scout — Docker Desktop built-in. Good for local dev. Requires Docker Hub account for full features.
Snyk — commercial, deep app-level analysis, developer-friendly reports. Free tier available.

For CI pipelines: Trivy is the industry standard. Zero cost, fast, comprehensive.
💡 Fix CVEs by updating base image
Most CVEs are in the base image OS packages. FROM node:20-alpine → 10 CVEs. Update to latest patch: FROM node:20.11.0-alpine → often 0 CVEs. Run docker pull node:20-alpine to get the latest digest.
Part 4 of 5

Registry Security — authentication, signing, retention

Pull secrets + image signing
# === GHCR: GITHUB_TOKEN (no setup needed) ===
# In GitHub Actions, just add permissions:
permissions:
  packages: write

# === GHCR: PAT for local push ===
# Create PAT: github.com/settings/tokens
# Scopes: write:packages, read:packages, delete:packages
echo $GITHUB_PAT | docker login ghcr.io \
  -u YOUR_USERNAME \
  --password-stdin

# === ACR: Service Principal (CI) ===
az acr login --name acrinfra
# Or with credentials:
docker login acrinfra.azurecr.io \
  -u $SP_CLIENT_ID \
  -p $SP_CLIENT_SECRET

# === K8s: imagePullSecret for private registry ===
kubectl create secret docker-registry ghcr-secret \
  --docker-server=ghcr.io \
  --docker-username=YOUR_USER \
  --docker-password=$GITHUB_PAT
# In pod spec:
spec:
  imagePullSecrets:
    - name: ghcr-secret

# === Image signing with cosign ===
# Install: brew install cosign
cosign generate-key-pair
cosign sign --key cosign.key \
  ghcr.io/user/myapp:v1.0.0
# Verify signature:
cosign verify --key cosign.pub \
  ghcr.io/user/myapp:v1.0.0
# K8s: Policy Controller enforces: only signed images run
Registry security checklist
  • Use private registries — production images should never be public
  • Enable vulnerability scanning — ACR Defender, GHCR security alerts, Trivy in CI
  • Use service principal / managed identity — not username/password where possible
  • Pull secrets in Kubernetes — configure imagePullSecrets for private registries
  • Sign images — cosign + K8s policy controller ensures only trusted images run
  • Retention policies — delete images older than N days / keep last N versions
  • Immutable tags — prevent overwriting production tags
ACR retention policy
# Delete images older than 30 days
az acr config retention update \
  --name acrinfra \
  --status enabled \
  --days 30 \
  --type UntaggedManifests

# Keep only last 10 tagged images per repo
az acr task create \
  --name cleanup-old-images \
  --registry acrinfra \
  --cmd "acr purge --filter 'myapp:.*' \
         --keep 10 --untagged" \
  --schedule "0 1 * * *"
Part 5 of 5

ACR — Azure Container Registry in production

ACR essential commands
# === Login ===
az acr login --name acrinfra
# uses az CLI credentials — no password in shell!

# === Build + push directly in Azure ===
az acr build \
  --registry acrinfra \
  --image myapp:v1.0.0 \
  .
# Builds in Azure, no local Docker needed!
# Great for CI runners without Docker daemon

# === List repositories ===
az acr repository list \
  --name acrinfra \
  --output table

# === List tags for a repo ===
az acr repository show-tags \
  --name acrinfra \
  --repository myapp \
  --orderby time_desc

# === ACR Defender: scan results ===
az security assessment list \
  --query "[?contains(name,'acr')]"

# === GitHub Actions → ACR (Managed Identity) ===
# In your workflow:
- uses: azure/login@v2
  with:
    client-id: \${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: \${{ secrets.AZURE_TENANT_ID }}
    subscription-id: \${{ secrets.AZURE_SUBSCRIPTION_ID }}

- uses: azure/docker-login@v1
  with:
    login-server: acrinfra.azurecr.io
    username: \${{ secrets.ACR_USERNAME }}
    password: \${{ secrets.ACR_PASSWORD }}
Why ACR for AKS
  • Same Azure region — AKS pulls from ACR in UAE North with zero egress cost. Docker Hub pull from UAE = latency + cost.
  • Managed identity auth — AKS has a system-assigned managed identity that can pull from ACR. No credentials, no rotation.
  • Geo-replication — replicate to UAE Central for DR. AKS in UAE Central pulls from local replica.
  • Defender for Containers — Microsoft's built-in scanner. Reports vulnerabilities in Azure Security Center.
  • ACR Tasks — build images in Azure triggered by base image updates. Auto-rebuild when node:20-alpine is patched.
💡 Attach AKS to ACR (no pull secrets needed)
az aks update -n myaks -g myrg --attach-acr acrinfra
This grants the AKS managed identity the AcrPull role on ACR. Pods can pull from ACR without configuring imagePullSecrets
Full CI/CD Pattern

Build → Scan → Push — the complete workflow

container-pipeline.yml — build → scan → push
name: Container CI/CD

on:
  push:
    branches: [ main ]
  push:
    tags: [ 'v*.*.*' ]    ← also runs on git tag

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: \${{ github.repository }}

jobs:
  build-scan-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write    ← for uploading scan results

    steps:
      - uses: actions/checkout@v4

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: \${{ github.actor }}
          password: \${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/\${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=raw,value=latest,enable=\${{ github.ref == 'refs/heads/main' }}

      - name: Build image (don't push yet)
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false            ← build only, scan first!
          load: true             ← load into local Docker
          tags: \${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Scan with Trivy (BLOCK on CRITICAL)
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/\${{ env.IMAGE_NAME }}:latest
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: '1'          ← fail if CRITICAL/HIGH found

      - name: Upload scan results to GitHub
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

      - name: Push to GHCR (only if scan passed)
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true             ← now push
          tags: \${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
Build → Scan → Push order matters
WRONG: push first, scan after
Vulnerable image is in your registry. Even if scan fails, the image is already there and can be pulled.

CORRECT: build → scan → only push if scan passes
The key is push: false + load: true on the first build step. Build locally, scan locally, then push only if clean.
SARIF — scan results in GitHub UI

SARIF (Static Analysis Results Interchange Format) uploads scan results to GitHub's Security tab. You can see:

  • All CVEs found in the image
  • Which branch/commit introduced them
  • Remediation recommendations
  • Dismissal history with notes

Repo → Security → Code scanning alerts.

Hands-On Lab

🔧 GHCR Push + Trivy Scan

Authenticate → multi-tag build → push to GHCR → scan with Trivy → add scan gate to CI pipeline

⏱ 30 minutes
GitHub PAT ready ✓
Trivy installed ✓
🔧 Lab — Steps

Push with proper tags & scan in CI

1
Create a GitHub PAT and authenticate to GHCR
GitHub → Settings → Developer settings → Personal access tokens → New token (classic) → select write:packages, read:packages. Export as GITHUB_PAT. Login: echo $GITHUB_PAT | docker login ghcr.io -u USERNAME --password-stdin
2
Build with SemVer + SHA + latest tags
Set VERSION=1.0.0, get SHA=$(git rev-parse --short HEAD). Build with three tags at once: $IMAGE:$VERSION, $IMAGE:$VERSION-$SHA, $IMAGE:latest.
3
Push all tags to GHCR
Push all three tags. Verify on GitHub: Repo → Packages tab → see your image with all three tags. Note the different digest for each.
4
Scan locally with Trivy
trivy image --severity HIGH,CRITICAL my-devops-app:v1.0.0. Compare alpine-based image vs node:20 (full). See the CVE count difference. Use --exit-code 1 to test the gate.
5
Add Trivy scan step to CI pipeline
Update docker.yml: add push: false + load: true to build step, then aquasecurity/trivy-action scan step, then second build step with push: true. Push → watch scan gate in Actions.
🔧 Lab — Complete Code

All lab commands

Local build + push + scan
# === Step 1: Authenticate ===
export GITHUB_PAT="ghp_yourtoken"
export GITHUB_USERNAME="priyakaviyil"

echo $GITHUB_PAT | docker login ghcr.io \
  -u $GITHUB_USERNAME \
  --password-stdin
# Login Succeeded

# === Step 2: Build with all tags ===
IMAGE="ghcr.io/$GITHUB_USERNAME/my-devops-app"
VERSION="1.0.0"
SHA=$(git rev-parse --short HEAD)

echo "Building $IMAGE:$VERSION-$SHA"

docker build \
  -t $IMAGE:$VERSION \
  -t $IMAGE:$VERSION-$SHA \
  -t $IMAGE:latest \
  -f Dockerfile .

# === Step 3: Push all tags ===
docker push $IMAGE:$VERSION
docker push $IMAGE:$VERSION-$SHA
docker push $IMAGE:latest

# Check: github.com/USERNAME/my-devops-app → Packages
# You should see: v1.0.0, v1.0.0-abc1234, latest

# === Step 4: Scan locally ===
# Install if needed:
# brew install aquasecurity/trivy/trivy

trivy image $IMAGE:$VERSION
trivy image --severity HIGH,CRITICAL $IMAGE:$VERSION

# Test the gate:
trivy image --exit-code 1 \
            --severity CRITICAL \
            $IMAGE:$VERSION
echo "Exit code: $?"
# 0 = no CRITICAL → CI would pass
# 1 = CRITICAL found → CI would fail
Updated docker.yml — with scan gate
name: Docker
on:
  push:
    branches: [ main ]
  push:
    tags: [ 'v*.*.*' ]

env:
  IMAGE: ghcr.io/\${{ github.repository }}

jobs:
  build-scan-push:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: \${{ github.actor }}
          password: \${{ secrets.GITHUB_TOKEN }}

      # Build WITHOUT pushing
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          load: true
          tags: \${{ env.IMAGE }}:scan-target
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Scan — fail on CRITICAL
      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: \${{ env.IMAGE }}:scan-target
          severity: CRITICAL,HIGH
          exit-code: '1'

      # Only push if scan passed
      - name: Push to GHCR
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            \${{ env.IMAGE }}:sha-\${{ github.sha }}
            \${{ env.IMAGE }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
Knowledge Check

Quiz Time

3 questions · 5 minutes · latest tag danger, Trivy, Git SHA tagging

QUESTION 1 OF 3
Why should you avoid :latest in production deployments?
A
:latest images are too large for production
B
:latest is mutable — two pulls at different times can give different image versions, causing inconsistency
C
:latest tags are not supported in Kubernetes
D
:latest images cannot be pushed to private registries
QUESTION 2 OF 3
What does Trivy scan Docker images for?
A
Performance bottlenecks and memory leaks
B
Syntax errors in Dockerfiles
C
Known CVEs in OS packages, language libraries, and binaries
D
Unused Docker layers and image size optimisation
QUESTION 3 OF 3
Which tagging strategy ties a Docker image to a specific Git commit?
A
:latest
B
SemVer tag like :v1.2.3
C
Git SHA tag like :sha-abc1234
D
Environment name like :production
Day 19 — Complete

What you learned today

🏪
Registries
Docker Hub (public), GHCR (GitHub), ACR (Azure), ECR (AWS). Right registry for right cloud.
🏷
Tagging
:latest = danger. SHA = traceability. SemVer = human-readable. Combined v1.0.0-sha = best.
🛡
Trivy
Scans OS + libs + binaries for CVEs. --exit-code 1 gates CI. Build → scan → push only if clean.
🔐
Security
PAT / managed identity auth. cosign for signing. Retention policies. Private registries for prod.
Day 19 Action Items
  1. 3 tags visible in GHCR Packages tab (v1.0.0, v1.0.0-SHA, latest) ✓
  2. Trivy scan: local result showing CVE counts ✓
  3. docker.yml updated: build → scan → push order ✓
  4. Commit: ci: add trivy scan gate before push
Tomorrow — Day 20 (Week 4 Capstone)
Containers Capstone Lab

End-to-end pipeline: Express API → optimised Dockerfile → Docker Compose → GitHub Actions (build → test → scan → push) → GHCR with SemVer tags triggered by git tag.
Full pipeline git tag trigger Week 4 complete
📌 Reference

Image tagging — complete strategy guide

Automated tagging in GitHub Actions
# metadata-action generates tags automatically
- id: meta
  uses: docker/metadata-action@v5
  with:
    images: ghcr.io/\${{ github.repository }}
    tags: |
      # Git SHA — always generated
      type=sha                             → sha-a3f7c2d

      # Branch name — on branch push
      type=ref,event=branch                → main

      # PR number — on pull request
      type=ref,event=pr                    → pr-42

      # Full SemVer — on git tag v1.2.3
      type=semver,pattern={{version}}      → 1.2.3

      # Minor version — on git tag
      type=semver,pattern={{major}}.{{minor}} → 1.2

      # Major version — on git tag
      type=semver,pattern={{major}}        → 1

      # latest — only on main branch
      type=raw,value=latest,\
        enable=\${{ github.ref == 'refs/heads/main' }}

# On push to main:
# Tags: sha-a3f7c2d, main, latest

# On git tag v2.4.1:
# Tags: sha-a3f7c2d, 2.4.1, 2.4, 2, latest
Trigger on git tag
# Workflow trigger
on:
  push:
    tags: [ 'v*.*.*' ]   ← semantic version tags

# Create and push a release tag:
git tag v2.4.1
git push origin v2.4.1

# → Triggers workflow
# → metadata-action generates:
#    2.4.1, 2.4, 2, sha-xxx, latest
# → Image pushed with all tags
⚠ Immutable tags in production
Configure your registry to prevent overwriting production tags. In GHCR: package settings → Protected tags. In ACR: az acr config content-trust enable. Never overwrite a tag that's currently deployed.
💡 Convention: tag before merge
Many teams create git tags after a PR is approved, before merging. The CI pipeline builds the versioned image automatically. The merge to main then deploys that exact tagged version.
📌 Reference

Trivy — complete command reference

Trivy scan modes
# === Image scanning ===
trivy image myapp:latest          # local image
trivy image ghcr.io/u/app:v1.0.0  # remote image
trivy image nginx:alpine           # official image

# === Severity filtering ===
trivy image --severity HIGH,CRITICAL myapp
trivy image --severity CRITICAL myapp

# === CI gate ===
trivy image --exit-code 1 \
            --severity CRITICAL \
            myapp:latest
# exit 0 = no CRITICAL → pipeline passes
# exit 1 = CRITICAL found → pipeline fails

# === Output formats ===
trivy image --format table myapp      # default
trivy image --format json myapp       # machine-readable
trivy image --format sarif myapp      # GitHub Security tab
trivy image --format cyclonedx myapp  # SBOM output

# === Filesystem scan (before build) ===
trivy fs .                            # scan current dir
trivy fs --severity HIGH,CRITICAL .

# === Repo scan ===
trivy repo github.com/user/repo

# === IaC scan ===
trivy config ./k8s/           # K8s YAML
trivy config ./terraform/     # Terraform
trivy config Dockerfile       # Dockerfile lint

# === Secret scanning ===
trivy fs --scanners secret .
# finds: AWS keys, GitHub tokens, etc.

# === Ignore specific CVEs ===
# Create .trivyignore:
# CVE-2023-1234   # accepted risk: no fix available
# CVE-2023-5678   # not exploitable in our config
Trivy in GitHub Actions — all options
- uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:latest      required
    format: sarif                 table|json|sarif
    output: trivy.sarif           output file
    severity: CRITICAL,HIGH       filter levels
    exit-code: '1'                fail CI if found
    ignore-unfixed: true          skip no-fix CVEs
    vuln-type: os,library         what to scan
    trivyignores: .trivyignore    ignore file
💡 ignore-unfixed: true for CI
Some CVEs have no fix available yet. Using ignore-unfixed: true skips CVEs where no patched version exists. Prevents false positives that block CI for issues you can't fix. Review unfixed CVEs separately.
SBOM — Software Bill of Materials
trivy image --format cyclonedx myapp generates an SBOM — a complete list of every package in your image. Required for compliance in financial, healthcare, and government sectors.
Week 4 Progress

Week 4 — 4 of 5 days complete

Day Topic Key Output Status
Day 16Docker FundamentalsNamespaces, cgroups, OverlayFS lab
Day 17Writing Dockerfiles1.1 GB → 80 MB optimised Dockerfile
Day 18Docker ComposeNode.js + PostgreSQL + Redis local stack
Day 19 ← TODAYContainer RegistriesSemVer+SHA tags + Trivy gate in CI
Day 20Container CapstoneFull container pipeline end-to-endTomorrow
Complete container pipeline after Day 19
git push → docker.yml triggers:
  1. Checkout
  2. Setup Buildx
  3. Login to GHCR (GITHUB_TOKEN)
  4. Build image (push: false, load: true)
  5. Trivy scan → exit 1 if CRITICAL ← NEW
  6. Push to GHCR with SHA + latest tags ← passes only if scan clean

git tag v1.0.0 → push tag →
  Same pipeline + semantic version tags:
  v1.0.0, 1.0, 1, sha-xxx, latest
Week 4 skills completed
  • ✅ Container internals (namespaces, cgroups)
  • ✅ Production Dockerfile (multi-stage, non-root)
  • ✅ Docker Compose (local full stack)
  • ✅ Registry push (GHCR + proper tags)
  • ✅ Vulnerability scanning (Trivy CI gate)
  • ⏳ Week 4 Capstone (tomorrow)
📌 Reference

Registry selection guide — which one when

Registry Free tier CI auth Scanning Best for
Docker Hub1 private, rate limitsSecretPaid tierPublic open-source images
GHCRUnlimitedGITHUB_TOKENDependabot alertsGitHub repos, open-source, courses ⭐
ACRPaid (Basic ~$5/mo)Managed IdentityDefender built-inAzure/AKS workloads ⭐
ECR500 MB/monthIAM roleInspectorAWS/EKS workloads
Artifact Registry (GCR)500 MB/monthWorkload IdentityContainer AnalysisGCP/GKE workloads
💡 For this course: use GHCR
Free, unlimited private repos, GITHUB_TOKEN needs zero setup, integrated with your GitHub repos. Perfect for learning. When you join a company with Azure: swap to ACR. Same docker commands, different registry URL.
⚠ Docker Hub rate limits in CI
Unauthenticated pulls from Docker Hub are limited to 100 per 6 hours. CI runners hit this quickly. Solution: authenticate to Docker Hub in CI, or use GHCR/ACR as a mirror for official images.
📌 Troubleshooting

Common registry & scan issues

Problem Cause Fix
GHCR push: denied (403)Missing permissions: packages: write or PAT missing write:packages scopeAdd permissions: packages: write to job. For PAT: ensure write:packages scope selected.
Trivy scan fails CI unexpectedlyCVEs found in base imageUpdate base image: docker pull node:20-alpine. Use ignore-unfixed: true for no-fix CVEs. Add .trivyignore for accepted risks.
Image not visible in GHCR Packages tabPackage is private or linked to different repoGitHub → your profile → Packages. Find the package. Package settings → Change visibility to public if needed.
docker pull: unauthorizedNot logged in or token expiredecho $PAT | docker login ghcr.io -u username --password-stdin. Check PAT hasn't expired.
Trivy: "unable to pull image"Image not in local Docker, registry auth neededLogin to registry first. For CI: ensure registry credentials are configured before Trivy scan step.
Docker Hub rate limit: 429 Too Many RequestsUnauthenticated CI pulls exhaustedAdd Docker Hub login step in CI. Or use GHCR/ACR as pull-through cache for official images.
git tag not triggering CIWorkflow only triggers on branch pushAdd tag trigger: on: push: tags: ['v*.*.*']
ACR login fails in CIService principal credentials expired or wrongaz acr login --name acrinfra to test locally. Regenerate SP credentials if expired.
📌 Day 19 Quick Reference

Everything at a glance

Registry operations
# Auth
echo $PAT | docker login ghcr.io -u user --password-stdin
az acr login --name acrinfra

# Build with proper tags
IMAGE="ghcr.io/user/myapp"
VERSION="1.0.0"
SHA=$(git rev-parse --short HEAD)
docker build \
  -t $IMAGE:$VERSION \
  -t $IMAGE:$VERSION-$SHA \
  -t $IMAGE:latest .

# Push
docker push $IMAGE:$VERSION
docker push $IMAGE:$VERSION-$SHA
docker push $IMAGE:latest

# Pull / inspect
docker pull $IMAGE:v1.0.0
docker inspect $IMAGE:v1.0.0

# List local images
docker images $IMAGE
docker image ls --digests $IMAGE

# Trivy scan
trivy image $IMAGE:v1.0.0
trivy image --severity HIGH,CRITICAL \
            --exit-code 1 \
            $IMAGE:v1.0.0

# Create release tag
git tag v1.0.0
git push origin v1.0.0
Tag cheatsheet
# ❌ Avoid in production
myapp:latest         ← mutable, no traceability

# ✅ Use for deployments
myapp:sha-a3f7c2d    ← immutable, traceable

# ✅ Use for releases
myapp:v1.2.3         ← human-readable, immutable

# ✅ Best for production (both)
myapp:v1.2.3-sha-abc ← version + commit

# Rollback with SHA:
# helm upgrade app chart/ \
#   --set image.tag=sha-a3f7c2d
.trivyignore — accepted risks
# .trivyignore — CVEs to skip
CVE-2023-1234  # no fix available yet
CVE-2023-5678  # not exploitable: internal only
# Always document WHY you're ignoring!
💡 Day 19 commit messages
ci: add trivy scan gate before push
release: tag v1.0.0