# 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`: ```dockerfile 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` ```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`: ```yaml - 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.yml` — `test`, `typecheck`, `lint` jobs Add after checkout in each job: ```yaml - 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