DevOps · 35 Days · Week 3 Day 14 — Artifacts & Package Management
1 / 22
Week 3 · Day 14

Artifacts & Package Management

Build artifacts, Dockerfiles, multi-stage builds, GHCR — build a Docker image in CI, push it to a registry tagged with the commit SHA, and scan dependencies for vulnerabilities. Code becomes a deployable, versioned unit.

⏱ Duration 60 min
📖 Theory 25 min
🔧 Lab 30 min
❓ Quiz 5 min
git push → image built → registry pushed Docker needed: docker --version
Session Overview

What we cover today

01
Build Artifacts — what they are
Docker images, npm packages, JARs, binaries. Versioned, immutable, deployable units.
02
Semantic Versioning (SemVer)
MAJOR.MINOR.PATCH. Git tags for releases. Triggering CI on tag push.
03
Dockerfile — writing production images
FROM, WORKDIR, COPY, RUN, EXPOSE, CMD. Multi-stage builds. Non-root user security.
04
Multi-stage Docker builds
Builder stage (with dev tools) → Runtime stage (lean, secure). 1.2 GB → 150 MB.
05
GHCR — GitHub Container Registry
Free registry built into GitHub. Push with GITHUB_TOKEN. Pull in deployments.
06
Dependency Security — npm audit
npm audit, Dependabot, Snyk, SBOM. Vulnerabilities in your dependencies are your vulnerabilities.
07
🔧 Lab — Docker Build + GHCR Push
Dockerfile → local build → docker.yml CI → push to ghcr.io on every merge to main.
Part 1 of 5

Build Artifacts — versioned deployable units

What is a build artifact?

The output of your build stage — a deployable, versioned unit of software. Once built, an artifact is immutable. You deploy the same artifact to staging and production.

Docker imageContainerised app — most common today
npm packageJavaScript library published to npm registry
JAR / WARJava application
BinaryCompiled Go / Rust / C++ executable
Helm chartKubernetes deployment package
dist/ folderBuilt front-end static files
The Artifact Principle
Build once, deploy many times.
The same Docker image that passes tests in CI is the exact image deployed to staging and then to production. No rebuilding on deploy — the artifact is already tested and trusted.
Semantic Versioning — MAJOR.MINOR.PATCH
MAJOR Breaking changes. Existing code may break. 2.0.0 → 3.0.0
MINOR New features, backward-compatible. 2.3.0 → 2.4.0
PATCH Bug fixes, backward-compatible. 2.4.0 → 2.4.1
# Tag a release:
git tag v2.4.1
git push origin v2.4.1
# Pre-release:
v2.4.0-rc.1 v2.4.0-beta.2
💡 Tag Docker images with commit SHA
In CI: myapp:$\{{ github.sha }}
Full traceability — every running container maps back to an exact commit. For releases: also tag myapp:v2.4.1 and myapp:latest.
Part 2 of 5

Dockerfile — instruction by instruction

Dockerfile — annotated
# === Every line creates a new LAYER ===

FROM node:20-alpine        # Base image. alpine = tiny (5 MB)

LABEL maintainer="devops@example.com"
LABEL version="1.0.0"

WORKDIR /app               # All subsequent commands run here

# COPY package*.json first (separate layer)
# Why: this layer is cached unless package.json changes
# npm install won't re-run if only src/ files changed
COPY package*.json ./
RUN npm ci --only=production  # prod deps only — no jest, eslint

# COPY source AFTER install (cache optimisation)
COPY src/ ./src/

# Security: never run as root in production!
RUN addgroup -S appgroup && \
    adduser  -S appuser -G appgroup
USER appuser

EXPOSE 3000                # Documents the port (doesn't publish)

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["node", "src/index.js"]  # Start command (exec form)
Key Dockerfile instructions
  • FROM — base image. Always use specific tags (node:20-alpine not node:latest)
  • WORKDIR — sets working directory for all following commands
  • COPY src dest — copy files from host into image
  • RUN cmd — execute during build, creates a layer
  • ENV KEY=val — set environment variable
  • EXPOSE port — documents which port the app uses
  • USER — switch to non-root user
  • HEALTHCHECK — Docker checks if container is healthy
  • CMD ["node", "app.js"] — default start command (exec form)
💡 Layer caching — order matters
Layers are cached. If a layer changes, all subsequent layers rebuild. Put frequently-changing files last.

✅ COPY package*.json → RUN npm install → COPY src/
❌ COPY . . → RUN npm install (src/ change = npm reinstall every time)
⚠ Never run as root
Default is root user. If container is compromised, attacker has root inside container. Always add a non-root user and switch with USER appuser before CMD.
Part 3 of 5

Multi-stage builds — lean production images

Dockerfile — multi-stage
# ═══ STAGE 1: Build ══════════════════════════
# Has: node, npm, all devDependencies, build tools
FROM node:20-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci                 # ALL deps including devDeps

COPY src/ ./src/
RUN npm run build          # transpile/bundle → dist/

# ═══ STAGE 2: Runtime ════════════════════════
# Has: ONLY what's needed to run in production
# No jest, eslint, typescript, webpack, etc.
FROM node:20-alpine AS runtime
WORKDIR /app

# Security: non-root user
RUN addgroup -S appgroup && \
    adduser  -S appuser -G appgroup

# Only copy what we need from builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist         ./dist

USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]

# === Size comparison ===
# Single stage (node:20):          ~1.1 GB
# Single stage (node:20-alpine):   ~170 MB
# Multi-stage (node:20-alpine):     ~80 MB
# Multi-stage (distroless):         ~50 MB
Why multi-stage matters
Single stage: Jest, ESLint, TypeScript compiler, webpack all end up in the production image. Attackers can exploit these tools.

Multi-stage: Runtime image contains only Node.js + your compiled app. No compilers, no test frameworks, no package manager. Smaller attack surface + faster pull times.
Build locally + test
# Build the image
docker build -t my-devops-app:local .

# Check the image size
docker images my-devops-app

# Run it locally
docker run -p 3000:3000 my-devops-app:local

# Test the app
curl http://localhost:3000

# Check running containers
docker ps

# Stop it
docker stop $(docker ps -q)
💡 .dockerignore — exclude unnecessary files
node_modules
.git
coverage/
*.md
.env
.github/

Without .dockerignore, COPY . . sends your entire node_modules into the Docker build context — slow and wasteful.
Part 4 of 5

GitHub Container Registry — push your image from CI

docker.yml — build & push to GHCR
name: Docker Build & Push

on:
  push:
    branches: [ main ]      # Push image on every merge
  push:
    tags: [ 'v*.*.*' ]      # Also push on git tags

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: \${{ github.repository }}  # owner/repo

jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write    # ← needed to push to GHCR

    steps:
      - uses: actions/checkout@v4

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

      # Extract metadata for tags
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/\${{ env.IMAGE_NAME }}
          tags: |
            type=sha                        # sha-abc1234
            type=ref,event=branch           # main
            type=semver,pattern={{version}} # v2.4.1

      # Build and push
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: \${{ steps.meta.outputs.tags }}
          labels: \${{ steps.meta.outputs.labels }}
          cache-from: type=gha    # GitHub Actions cache
          cache-to: type=gha,mode=max
GHCR vs Docker Hub
FeatureGHCRDocker Hub
Auth in CIGITHUB_TOKEN (auto)Need secret
Free private✅ Unlimited1 repo free
Rate limitsNone (logged in)100/6h anon
Locationghcr.io/owner/repodocker.io/user/repo
GITHUB_TOKEN — no secret setup needed
secrets.GITHUB_TOKEN is automatically available in every workflow run. You don't need to create it — GitHub provides it. For GHCR, just add permissions: packages: write to the job.
💡 Docker layer cache with GitHub Actions
cache-from: type=gha uses GitHub Actions cache storage for Docker layers. First build: 2 min. Subsequent builds (no code change): 15 sec. Cache layer by layer.
Part 5 of 5

Dependency Security — npm audit & vulnerability scanning

npm audit commands
# === Basic scan ===
npm audit
# Output:
# found 3 vulnerabilities (1 moderate, 2 high)
# Run 'npm audit fix' to fix them

# === Auto-fix safe updates ===
npm audit fix

# === Fix including breaking changes ===
npm audit fix --force   # careful — may break things

# === JSON output for CI parsing ===
npm audit --json | jq '.metadata.vulnerabilities'

# === Fail CI if HIGH or CRITICAL vulns ===
npm audit --audit-level=high
# exits 1 if any HIGH/CRITICAL found → CI fails

# === List all outdated packages ===
npm outdated

# === Check specific package ===
npm audit --json | jq '.vulnerabilities | keys[]'
Security in CI pipeline
- name: Security audit
  run: npm audit --audit-level=high
  # Fails if HIGH or CRITICAL vulnerabilities found

- name: Scan Docker image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ghcr.io/\${{ env.IMAGE_NAME }}:latest
    format: table
    exit-code: '1'           # Fail if vulns found
    severity: HIGH,CRITICAL
Dependency security tools
  • npm audit — built-in. Checks npm advisory database. Run npm audit --audit-level=high in CI.
  • Dependabot — GitHub native. Automatically opens PRs for outdated/vulnerable deps. Configure in .github/dependabot.yml.
  • Snyk — deep scanning. Tracks vulnerabilities across code, deps, Docker images. Free tier available.
  • Trivy — scans Docker images for OS + package vulnerabilities. Used by many enterprises.
  • SBOM — Software Bill of Materials. List of every dependency. Required for compliance.
.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
⚠ Your deps are your attack surface
Log4Shell (2021): a Java logging library used by thousands of apps had a critical vulnerability. Companies with no visibility into their dependencies were exposed without knowing. Automate dependency scanning — don't audit manually.
Hands-On Lab

🔧 Docker Build & GHCR Push

Write a Dockerfile → build locally → push to GHCR via CI → verify image at ghcr.io

⏱ 30 minutes
Docker Desktop ✓
my-devops-app ✓
🔧 Lab — Steps

Build & push your first Docker image in CI

1
Create a minimal Node.js app (src/index.js)
A simple HTTP server on port 3000 with a /health endpoint. This is what gets containerised.
2
Write the Dockerfile (multi-stage)
Builder stage: npm ci (prod deps). Runtime stage: copy node_modules + src, add non-root user. Add .dockerignore to exclude node_modules and .git.
3
Build and test locally
docker build -t my-devops-app:local .docker run -p 3000:3000 my-devops-app:localcurl localhost:3000/health → 200 OK.
4
Create .github/workflows/docker.yml
Triggers on push to main. Login to GHCR with GITHUB_TOKEN. Build + push using docker/build-push-action. Tags: SHA + latest.
5
Push and verify in GHCR
Merge to main → workflow runs → check GitHub repo → Packages tab → see your image at ghcr.io/username/my-devops-app. ✅
6
Add npm audit to CI
Add npm audit --audit-level=high step before Docker build. If you have high/critical vulnerabilities, fix them first.
🔧 Lab — Files

Complete lab code

src/index.js — minimal HTTP server
const http = require('http');

const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', version: '1.0.0' }));
  } else {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello from my-devops-app!\n');
  }
});

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = server; // for testing
Dockerfile
# Stage 1: Build (has devDeps + build tools)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/

# Stage 2: Runtime (lean, secure)
FROM node:20-alpine AS runtime
WORKDIR /app
RUN addgroup -S appgroup && \
    adduser  -S appuser -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
USER appuser
EXPOSE 3000
HEALTHCHECK CMD wget -qO- http://localhost:3000/health
CMD ["node", "src/index.js"]
.dockerignore
node_modules
.git
coverage
*.md
.env
.github
tests
.github/workflows/docker.yml
name: Docker Build & Push

on:
  push:
    branches: [ main ]

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

jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node + audit
        uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }

      - run: npm ci
      - run: npm audit --audit-level=high

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/\${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=raw,value=latest

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: \${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
Knowledge Check

Quiz Time

3 questions · 5 minutes · SemVer, multi-stage builds, npm audit

Day 14 knowledge check →
QUESTION 1 OF 3
In Semantic Versioning 2.4.1, what does the middle number 4 represent?
A
Major version — breaking changes
B
Minor version — new backward-compatible features
C
Patch version — backward-compatible bug fixes
D
Build number — assigned by CI
QUESTION 2 OF 3
What is the main benefit of multi-stage Docker builds?
A
Faster runtime performance of the application
B
Smaller, more secure production images by keeping build tools out of the runtime stage
C
Better developer experience during local development
D
Faster CI build times
QUESTION 3 OF 3
Which command scans your npm dependencies for known security vulnerabilities?
A
npm test
B
npm check
C
npm audit
D
npm scan
Day 14 — Complete

What you learned today

📦
Artifacts
Build once, deploy many. Docker image = immutable versioned unit. Tag with git SHA.
🐳
Dockerfile
Multi-stage: builder (devDeps) → runtime (lean). Non-root user. Layer cache order matters.
📤
GHCR
Free registry in GitHub. GITHUB_TOKEN auto-auth. docker/build-push-action in CI.
🛡
npm audit
Scans for known CVEs. --audit-level=high fails CI on HIGH/CRITICAL. Dependabot for auto-PRs.
Day 14 Action Items
  1. Docker image visible at ghcr.io/username/my-devops-app
  2. Multi-stage Dockerfile in repo root ✓
  3. npm audit passing (no high/critical) ✓
  4. Commit: ci: add docker build and push to ghcr
Tomorrow — Day 15 (Week 3 Final)
Continuous Deployment

Deploy to staging automatically. Production with manual approval gate. Deployment strategies (rolling, blue-green, canary). Rollback plan.

GitHub Environments approval gate rollback
📌 Reference

Dockerfile — complete instruction reference

All Dockerfile instructions
# === Base ===
FROM node:20-alpine         # Base image (ALWAYS use specific tag)
FROM scratch               # Empty base (for static binaries)
FROM node:20 AS builder   # Named stage for multi-stage

# === Metadata ===
LABEL key="value"          # Key-value metadata
ARG VERSION=1.0            # Build-time variable (not in image)
ENV NODE_ENV=production    # Runtime environment variable

# === Filesystem ===
WORKDIR /app               # Set working directory
COPY src/ ./src/           # Copy from host to image
COPY --from=builder /app . # Copy from another stage
ADD archive.tar.gz /app/   # Like COPY but auto-extracts

# === Execution ===
RUN npm ci                 # Run during build (creates layer)
RUN ["npm", "ci"]          # Exec form (no shell)
CMD ["node", "app.js"]    # Default start command (overridable)
ENTRYPOINT ["node"]       # Fixed executable (CMD appends args)

# === Network / Volumes ===
EXPOSE 3000                # Document port (doesn't publish)
VOLUME ["/data"]           # Declare persistent data mount point

# === Security ===
USER appuser               # Switch to non-root user
HEALTHCHECK --interval=30s \
  CMD wget -qO- localhost:3000/health

# === Build ===
docker build -t name:tag .
docker build --target builder .  # Build specific stage only
docker build --no-cache .        # Ignore layer cache
CMD vs ENTRYPOINT

CMD ["node", "app.js"] — default command, easily overridden:
docker run myapp node other.js ← replaces CMD

ENTRYPOINT ["node"] — fixed executable, CMD becomes args:
docker run myapp app.js ← runs node app.js

Best practice: use ENTRYPOINT for the executable + CMD for default args.

ARG vs ENV
  • ARG — build-time only. Available during docker build, not in running container. Use for: version numbers, build timestamps.
  • ENV — runtime. Available in the running container. Use for: NODE_ENV, PORT.
ARG VERSION=1.0
ENV APP_VERSION=$VERSION   # Set ENV from ARG
📌 Reference

Docker CLI — essential commands

Image operations
# Build
docker build -t myapp:1.0.0 .
docker build -t myapp:1.0.0 -f Dockerfile.prod .

# List images
docker images
docker images myapp             # filter by name

# Tag
docker tag myapp:1.0.0 ghcr.io/user/myapp:1.0.0
docker tag myapp:1.0.0 myapp:latest

# Push / Pull
docker push ghcr.io/user/myapp:1.0.0
docker pull ghcr.io/user/myapp:latest

# Remove
docker rmi myapp:1.0.0
docker image prune              # remove dangling images

# Inspect
docker inspect myapp:1.0.0
docker history myapp:1.0.0     # show layers
docker image ls --format "table {{.Repository}}\t{{.Size}}"
Container operations
# Run
docker run -p 3000:3000 myapp:1.0.0
docker run -d myapp:1.0.0           # detached (background)
docker run --name mycon myapp:1.0.0
docker run -e NODE_ENV=prod myapp   # env var
docker run -v /host:/container myapp # volume mount

# List containers
docker ps                           # running
docker ps -a                        # all (including stopped)

# Interact
docker exec -it mycon sh            # shell in container
docker logs mycon                   # view logs
docker logs -f mycon                # follow logs

# Stop / remove
docker stop mycon
docker rm mycon
docker stop $(docker ps -q)         # stop all

# Cleanup
docker system prune                 # remove all unused
docker system df                    # disk usage
📌 Best Practice

Image tagging strategy — traceability & rollback

Tagging strategy in CI
# === Tag with multiple identifiers ===
# using docker/metadata-action@v5:

tags: |
  type=sha                    # sha-abc1234 ← always unique
  type=raw,value=latest       # latest ← always current
  type=ref,event=branch       # main ← branch name
  type=semver,pattern={{version}}  # v2.4.1 ← git tag
  type=semver,pattern={{major}}.{{minor}}  # v2.4

# === Result for push to main: ===
# ghcr.io/user/myapp:sha-a3f7c2d ← unique, immutable
# ghcr.io/user/myapp:main         ← always latest on main
# ghcr.io/user/myapp:latest       ← latest of all builds

# === Result for git tag v2.4.1: ===
# ghcr.io/user/myapp:v2.4.1       ← semantic version
# ghcr.io/user/myapp:2.4          ← minor version
# ghcr.io/user/myapp:latest       ← latest

# === Deploying: use SHA tag ===
# helm upgrade myapp chart/ \
#   --set image.tag=sha-a3f7c2d
# → Exact reproducible deployment, easy rollback
Why SHA tags for deployments
myapp:latest is mutable — it changes on every push. If you deploy :latest and something breaks, you can't roll back to "the previous latest".

myapp:sha-a3f7c2d is immutable — always the same image. To roll back: deploy the previous SHA tag. Perfect audit trail.
Trigger pipeline on git tag
# In GitHub Actions:
on:
  push:
    tags: [ 'v*.*.*' ]

# Trigger with:
git tag v2.4.1
git push origin v2.4.1
# → Runs docker.yml → image tagged v2.4.1
Week 3 Progress

Week 3 — 4 of 5 days complete

Day Topic Lab Output Status
Day 11CI/CD Conceptsci.yml live in Actions tab
Day 12Pipeline Deep DiveMulti-job matrix + Jenkins parallel
Day 13Testing in CIJest + 70% coverage gate
Day 14 ← TODAYArtifacts & DockerDocker image pushed to GHCR on merge
Day 15Continuous DeploymentStaging auto + prod approval gateTomorrow
Your pipeline files at end of Week 3
my-devops-app/
├── .github/workflows/
│   ├── ci.yml              ← Day 11: basic CI
│   ├── ci-advanced.yml     ← Day 12: matrix
│   ├── test.yml            ← Day 13: Jest
│   ├── docker.yml          ← Day 14: GHCR ← new
│   └── deploy.yml          ← Day 15: CD
├── Dockerfile              ← Day 14: multi-stage
├── .dockerignore           ← Day 14
├── src/ tests/ jest.config.js  ← Day 13
Full pipeline capability by Day 15
  • ✅ Lint + unit tests on every PR
  • ✅ 70% coverage gate enforced
  • ✅ Docker image built + pushed on merge
  • Auto-deploy to staging on merge
  • Manual approval → production deploy
📌 Best Practices

Docker security & optimisation checklist

✅ Security checklist
  • Use specific base image tags (node:20-alpine not node)
  • Run as non-root user (USER appuser)
  • Use --only=production in npm install (no devDeps)
  • Add HEALTHCHECK for container orchestration
  • Scan image with Trivy before pushing
  • Never store secrets in Dockerfile — use env vars at runtime
  • Use multi-stage to keep build tools out of runtime
  • Keep base images updated (Dependabot for Docker)
⚡ Optimisation checklist
  • .dockerignore — exclude node_modules, .git, tests
  • Copy package*.json before COPY . . — cache npm install layer
  • Use alpine variants (node:20-alpine vs node:20 = 5 MB vs 350 MB)
  • Combine RUN commands with && to reduce layers
  • Use BuildKit and layer cache in CI (cache-from: type=gha)
  • Use multi-stage — strip dev dependencies from final image
  • Use npm ci --only=production in runtime stage
Image size comparison
node:20              ≈ 350 MB  ← never use in prod
node:20-alpine       ≈   5 MB base
Single stage         ≈ 170 MB  (with node_modules)
Multi-stage alpine   ≈  80 MB  ← good for most apps
distroless           ≈  50 MB  ← best security
💡 distroless images (advanced)
FROM gcr.io/distroless/nodejs20-debian12 — no shell, no package manager, no OS utilities. Smallest attack surface possible. Used by Google, Kubernetes. Harder to debug but most secure.
📌 Troubleshooting

Common Docker & CI issues & fixes

Problem Cause Fix
docker build fails "COPY failed"File doesn't exist in build contextCheck path in COPY instruction. Run from directory containing Dockerfile. Check .dockerignore isn't excluding needed files.
GHCR push: "denied"Missing permissions: packages: writeAdd permissions: packages: write to the job in your workflow.
Container exits immediatelyApp crashes on startupdocker logs container-name to see the error.
Large image sizenode_modules in final stage, no .dockerignoreUse multi-stage build. Add .dockerignore. Use alpine base. docker history image-name shows which layers are large.
npm ci fails in Dockerpackage-lock.json not copiedUse COPY package*.json ./ (copies both package.json AND package-lock.json).
CI Docker build slow (no cache)No cache configuredAdd cache-from: type=gha and cache-to: type=gha,mode=max to build-push-action.
npm audit fails CIHigh/critical vulnerability foundnpm audit fix locally. Commit updated package-lock.json. If unfixable, use --audit-level=critical.
Port not accessible on localhostForgot -p flagdocker run -p 3000:3000 myapp — host:container port mapping required.
📌 Day 14 Quick Reference

Everything at a glance

Minimal production Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src/ ./src/

FROM node:20-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app .
USER app
EXPOSE 3000
CMD ["node", "src/index.js"]
.dockerignore
node_modules
.git
coverage
*.md
.env
.github
tests
docker.yml — minimal GHCR push
name: Docker
on:
  push:
    branches: [ main ]
jobs:
  push:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: \${{ github.actor }}
          password: \${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/\${{ github.repository }}:\${{ github.sha }}
            ghcr.io/\${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
💡 Day 14 commit message
ci: add docker multi-stage build and ghcr push