brackt/docs/ci-build-optimization.md
Chris Parsons 3f9c1d99e2
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m25s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m14s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
Fix invite link showing http and show draft board card during live draft
- Use x-forwarded-proto header (allowlisted to http/https only) when
  building the invite link origin so it shows https behind a TLS proxy
- Show the "View Draft Board" bare card in the right column while a
  draft is running, matching the card already shown for active/completed
  seasons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:31:52 -07:00

4.6 KiB

CI Build Optimization Plan

The Forgejo Actions build pipeline currently takes ~15 minutes on main branch pushes:

  • ~5 min: setup-buildx-action
  • ~5 min: build and push to container registry
  • ~5 min: deploy job (docker pull + migrate + compose up)

This plan cuts that to ~3-5 minutes for typical code changes.


Root Causes

1. Dockerfile layer invalidation (biggest win)

The development-dependencies-env stage copies all source files before running npm ci:

FROM node:20-alpine AS development-dependencies-env
COPY . /app          # <-- invalidates npm cache on EVERY commit
WORKDIR /app
RUN npm ci

Every single push blows the npm install cache layer, even if package.json hasn't changed. This means:

  • The build job re-runs npm ci inside Docker on every commit (~1-2 min)
  • The server's docker compose pull re-downloads the large node_modules layer (~200-300MB) on every deploy

The production-dependencies-env stage already does this correctly — the dev stage needs the same fix.

2. Buildx setup taking 5 minutes

setup-buildx-action normally takes ~20-30 seconds. 5 minutes means QEMU multi-platform emulators are being installed (for arm64 support). Since we only target linux/amd64, explicitly declaring the platform skips QEMU installation entirely.

3. No npm cache in CI jobs

The test, typecheck, and lint jobs each run npm ci independently with no caching. This adds ~30-60s per job on cold runners.


Changes

Fix 1: Dockerfile — copy only manifests before npm ci

File: Dockerfile

FROM node:20-alpine AS development-dependencies-env
COPY package.json package-lock.json .npmrc /app/   # only manifests
WORKDIR /app
RUN npm ci

FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json .npmrc /app/
WORKDIR /app
RUN npm ci --omit=dev

FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build

FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
COPY --from=build-env /app/dist /app/dist
COPY ./drizzle /app/drizzle
COPY ./scripts /app/scripts
COPY ./instrument.server.mjs /app/instrument.server.mjs
WORKDIR /app
CMD ["npm", "run", "start"]

Fix 2: Declare platform in build-push-action

File: .forgejo/workflows/deploy.yml — the build job's "Build and Push" step

Add platforms: linux/amd64:

- name: 🗄️ Build and Push to Container Registry
  uses: https://github.com/docker/build-push-action@v5
  with:
    context: .
    push: true
    platforms: linux/amd64
    tags: ${{ vars.CONTAINER_REGISTRY }}/brackt:latest
    cache-from: type=registry,ref=${{ vars.CONTAINER_REGISTRY }}/brackt:buildcache
    cache-to: type=registry,ref=${{ vars.CONTAINER_REGISTRY }}/brackt:buildcache,mode=max

Fix 3: Add npm cache to CI jobs

File: .forgejo/workflows/deploy.ymltest, typecheck, lint jobs

Add after checkout in each job:

- name: 📦 Cache node_modules
  uses: https://github.com/actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

Note: If Forgejo's runner doesn't support the cache action, this silently no-ops (harmless). An alternative is merging the three parallel jobs into one that installs once and runs all checks sequentially — unconditionally saves two npm ci calls.


Expected Results

docker pull on the server is incremental — it only downloads layers that changed since the last deploy. After the Dockerfile fix, most deploys only pull the small build-output layer instead of the full node_modules.

Step Before After (code change) After (dep change)
Setup buildx ~5 min ~30 sec ~30 sec
Build + push ~5 min ~1-2 min ~3-4 min
Deploy (docker pull + up) ~5 min ~1-2 min ~3-4 min
Total ~15 min ~3-5 min ~8-10 min

Verification

  1. Push a code-only commit (no package.json change) — confirm Docker build uses registry cache for npm install layers and the build step completes in under 2 minutes
  2. Push a commit changing package.json — confirm npm cache is correctly invalidated and reruns
  3. Check the setup-buildx-action log — should show ~30s, no QEMU download
  4. Check deploy job — docker compose pull should show most layers as Already exists
  5. Confirm deployed app is functional