Skip to content

Build & Deploy

Status: Audit v0.1 (2026-05-09).

Existing project documentation ../07-build-and-deploy.md covers the Dockerfile stages, the EFS mount semantics, and the Jenkins pipeline (including the warning-grep / exit-code-100 mechanism) in narrative depth. This audit doc focuses on operational maturity and findings; the dev doc focuses on developer mental model. Read both.

1. Build tool

Property Value Source
Framework Nuxt 2.18.1 package.json:67
Bundler Webpack 4 (bundled with Nuxt 2) package.json:113
Babel @babel/plugin-proposal-private-methods configured nuxt.config.js:325-330
transpile includes @fullcalendar.*, swr nuxt.config.js:331
Polotno bundler Parcel 2 polotno-editor/package.json

2. Build command

yarn build      # → nuxt build (writes .nuxt/)
yarn generate   # → nuxt generate (writes dist/ for static hosting)
yarn start      # → nuxt start (runs the SPA server from .nuxt/dist/)

The runtime in production is yarn start (Dockerfile final stage runs nuxt start). yarn generate produces the static-site snapshot in dist/, but per the start.sh deploy script that runs the nuxt start server, the live app is dynamic, not static.

The dist/ directory committed in the repo (78 MB) is a snapshot from a nuxt generate run; it is not the active production artifact.

3. Build time

[VERIFY-BD-1] measure on a developer machine. Expect 2–5 minutes for a clean build of both projects, given the size.

4. Build artifact

Path Contents
.nuxt/dist/client/*.js Per-route JS chunks for the SPA
.nuxt/dist/server/ Server-side runtime (small for ssr: false)
dist/_nuxt/*.js (only present after nuxt generate) — same chunks as .nuxt/dist/client but at the static-site root
polotno-bundle.js, polotno-bundle.css Output of polotno-editor Parcel build, written to repo root

Sizes are inventoried in performance.md.

5. Source maps

ls dist/_nuxt/*.js.map  # → empty
ls .nuxt/dist/client/*.js.map  # → may be present in dev

Production builds do not ship source maps. Confirmed via ls dist/_nuxt/*.js.map. This is good for security (no code disclosure) but bad for observability — production stack traces are minified and unusable without uploading source maps to an error tracker (which doesn't exist — see observability.md).

nuxt.config.js:312-318 sets dev-only source-map config:

build: {
  extend(config, ctx) {
    if (ctx.isDev) {
      config.devtool = ctx.isClient ? 'source-map' : 'inline-source-map'
    }
  }
}

In production (!ctx.isDev), devtool is unset; webpack 4's default for production is eval (for in-browser) or no source map for the build output. Confirmed by the empty grep.

Finding [WC-BD-1] (Severity: Medium): Production source maps are not uploaded to an error tracker (no error tracker exists). When errors do occur in production, stack traces are unreadable. Phase 0 — set up Sentry, upload source maps to Sentry, don't ship them to clients.

6. Deployment target

The production deploy is AWS Lightsail for dev_app per .github/workflows/dev_app.yml:1. UAT runs on AWS ECS per .github/workflows/uat_app_ecs.yml.

Two parallel CI/CD pipelines exist:

  1. GitHub Actions → AWS Lightsail (the live dev_app.yml workflow). Deploys dev_app branch.
  2. Jenkins → SSH → EC2 (the existing Jenkinsfile). Deploys dev_app to a configured server (54.190.153.94).

These are likely alternative deploy paths for the same environment, with one being current and the other legacy. [VERIFY-BD-2]: which is the actual current deploy mechanism? If both run, they may conflict.

The Jenkinsfile was inspected for 01-local-setup.md. The GH Actions workflows are newer (existing project doc 07-build-and-deploy.md does not yet describe them — it should be updated).

7. The Dockerfile

Dockerfile (referenced in 07-build-and-deploy.md) is a 3-stage build:

  1. Build polotno-editor (Parcel) → polotno-bundle.* to /tmp
  2. Build Nuxt main app (webpack) with the polotno bundle copied in → .nuxt/
  3. Runtime: node:20.18.3-slim + nginx, with node_modules mounted from EFS at runtime via start.sh

Findings already documented: - start.sh waits up to 60s for EFS node_modules to mount; a missing mount = boot failure. - The nuxt symlink in node_modules/.bin/ is recreated at boot if missing (EFS sometimes loses symlinks). - nginx fronts Nuxt on port 80. - client_max_body_size 200M for media uploads. - No security headers in nginx.conf (cross-ref security.md § WC-SEC-7).

8. CI/CD pipeline

8.1 GitHub Actions — dev_app.yml

Triggers on push to dev_app. The first ~150 lines do not run a build; they extract commit info and PR details for Teams / Slack notification (the actual build is later in the file — [VERIFY-BD-3] read the full file and capture the build step).

Hypothesis: the workflow likely:

  1. Checks out code
  2. Builds the Docker image
  3. Pushes to ECR (or Lightsail's container registry)
  4. Triggers a Lightsail deployment

Confirm with full .github/workflows/dev_app.yml review.

8.2 GitHub Actions — uat_app_ecs.yml

Targets ECS for UAT. Mechanically similar: build, push, deploy. [VERIFY-BD-4] read full file.

8.3 Jenkins (legacy)

Jenkinsfile deploys via SSH to 54.190.153.94. PM2 process dev_app. Probably superseded by GitHub Actions but left in place.

The Jenkins pipeline's warning-grep / exit-code-100 mechanism is documented in detail in ../07-build-and-deploy.md § "Build warnings → exit code 100". The 16 critical-warning patterns the pipeline scans for include:

  • Two component files resolving to the same name
  • component files resolving to the same name, Duplicate file
  • Module not found, Cannot resolve, Failed to resolve
  • WARN, WARNING, deprecated
  • compilation error, Failed to compile, Type error, TypeError
  • SyntaxError, Parse error, Unexpected token
  • Icon not found, Image not found, File not found, Cannot find, ENOENT
  • months old, update-browserslist-db

A match → exit code 100 → "succeeded with warnings" Teams notification, but the deploy still happens. Treat exit-100 deploys as PR cleanup, not "good enough" (per ../20-contributing.md § "Common failure modes").

Finding [WC-BD-2] (Severity: Medium): Two parallel CI/CD systems (GitHub Actions + Jenkins). At best, one is dead code; at worst, both run on dev_app push and conflict. Audit and decommission one.

9. Preview deployments

There is no vercel.json, netlify.toml, or equivalent. No preview-per-PR deploys observed. .github/workflows/dev_app.yml triggers only on push: branches: [dev_app].

Finding [WC-BD-3] (Severity: Medium): No preview deployments. Reviewers cannot see PR-as-deployed before merging. Combined with no automated tests (testing.md), this means review is "trust the diff." Phase 0/1 — add per-PR deploy via GitHub Actions to a Lightsail preview slot or to a temporary subdomain.

10. Rollback

The deploy script in Jenkinsfile does pm2 reload or starts fresh. There is no documented rollback recipe in this repo.

[VERIFY-BD-5]: how does the team revert a bad dev_app deploy? Manual git revert + push, or is there a runtime rollback mechanism?

For Lightsail / ECS, the natural rollback is to redeploy the previous Docker image tag.

Finding [WC-BD-4] (Severity: Medium): Rollback mechanism is undocumented. Phase 0 — write a one-page runbook for "the dev_app deploy is broken, restore previous version."

11. Environment configuration

Environment Branch Deploy target Backend (API_URL)
Dev dev_app AWS Lightsail (dev backend — verify)
UAT uat_app AWS ECS (UAT backend — verify)
Prod main ? (verify) (prod backend — verify)

[VERIFY-BD-6]: confirm the branch → environment mapping. The .github/workflows/uat_app_ecs.yml is the strongest evidence for UAT-from-uat_app; prod has no visible workflow.

Env vars are injected via Docker --build-arg (per Dockerfile); the GitHub Actions workflows must be passing these as build-args.

12. CDN / static asset serving

  • dist/_nuxt/*.js chunks are content-hashed (e.g., db8106d.js).
  • nginx.conf does not set Cache-Control headers — relies on browser ETag.
  • Service worker (@nuxtjs/pwa) caches /_nuxt/* cache-first.
  • No CloudFront or other CDN layer is documented in this repo. [VERIFY-BD-7]: is CloudFront in front of Lightsail? Most production AWS setups put one there.

(Cross-ref performance.md § Caching for the cache-control improvements.)

13. Dockerfile configuration risks

Concern Status
Image base node:20.18.3-slim — Node 20 LTS (good); minor version pinned (good)
Image size ~80 MB main Node + nginx + EFS-mounted node_modules (large)
.dockerignore Excludes node_modules, dist, .nuxt, .git, .github, etc. — good
Non-root user Not set — container runs as root
HEALTHCHECK curl -f http://localhost:80/ every 30s
Secrets via build-args Yes (env vars baked into bundle at build time, then .env deleted in stage 2)

Finding [WC-BD-5] (Severity: Medium): Container likely runs as root. [VERIFY-BD-8] add a USER node directive in stage 3.

Finding [WC-BD-6] (Severity: Medium): HEALTHCHECK only verifies nginx; doesn't verify the Nuxt process is alive (a wedged Nuxt process behind a healthy nginx fails 502s but passes the healthcheck). Add a deeper check, e.g., curl -f http://localhost:80/heartbeat (assuming /heartbeat returns something specific).

14. EFS-mounted node_modules

start.sh waits up to 60s for /usr/src/app/node_modules and /usr/src/app/polotno-editor/node_modules to be EFS-mounted. The architecture is deliberate (saves image space) but fragile:

  • An EFS outage = container can't boot.
  • Symlinks may not survive EFS round-trips (script recreates node_modules/.bin/nuxt if missing).
  • Layer caching is not used for node_modules.

Finding [WC-BD-7] (Severity: Medium): EFS-mounted node_modules is an unconventional choice. The savings (image size) are modest; the fragility is real. Recommendation: during the migration to Vue 3 / Nuxt 3, reconsider — bake node_modules into the image as a normal Docker layer.

15. Recommendations summary

Priority Action Effort
Phase 0a Verify which CI/CD is canonical (GH Actions vs Jenkins); decommission the other <1d
Phase 0a Read full .github/workflows/dev_app.yml and uat_app_ecs.yml; document the actual pipeline in this doc 1d
Phase 0a Document the rollback procedure <1d
Phase 0 Add preview-per-PR deploys 1 week
Phase 0 Set up Sentry; upload source maps from CI 1 week
Phase 0 Add Cache-Control: immutable for /_nuxt/ assets in nginx <1d
Phase 0 Add security headers in nginx (security.md § WC-SEC-7) 1d
Phase 1 Bake node_modules into Docker image; remove EFS dependency 1 week
Phase 1 Run container as non-root; tighten HEALTHCHECK 1d

16. Open questions

Tracked in ./verify-markers.md:

  • [VERIFY-BD-1] Build time on a developer machine
  • [VERIFY-BD-2] Canonical CI/CD: GH Actions vs Jenkins
  • [VERIFY-BD-3] Full dev_app.yml build steps
  • [VERIFY-BD-4] Full uat_app_ecs.yml build steps
  • [VERIFY-BD-5] Rollback mechanism
  • [VERIFY-BD-6] Branch → environment → backend mapping
  • [VERIFY-BD-7] CloudFront / CDN in front of Lightsail
  • [VERIFY-BD-8] Container user (root or non-root)