mobtranslate.com / docs › MobTranslate operations source: mobtranslate-next16-build-incident.md

MobTranslate Next 16 Build Incident and Deployment Runbook

Incident date: 2026-07-14 UTC
Application: mobtranslate.com
Repository: /mnt/donto-data/workspace/mobtranslate.com
Web application: apps/web
Framework: Next.js 16.2.9, React 19.2.7, Tailwind CSS 4.3.1, pnpm workspace
Final deployed build: ZS7-SuKDwqjovoz_tBnyE
Status: resolved and live
Scope: build, output tracing, deployment, service readiness, and browser verification; no model weights or linguistic data were changed by this incident response.

1. Executive ruling

The reported build problem was real, but it was not one isolated Next.js compiler bug. Four conditions compounded:

  1. The application explicitly forced the legacy Webpack path on Next 16 and retained a custom webpack alias hook that duplicated supported monorepo configuration.
  2. Retained .next.* release and rollback trees lived under the source application. Tailwind v4's automatic source detection could traverse them because the pattern was not ignored.
  3. Several runtime filesystem boundaries were too broad for Next's output-file tracer. One route trace reached 6,572 files, including 4,547 files from the live .next tree.
  4. The shared /dev/sdb data disk was saturated by concurrent repository and research scans. This made both builds and the first post-deploy request appear hung even after the code-level causes were fixed.

The final remedy was to restore Next 16's native Turbopack path, use supported monorepo settings, move retained build trees outside the source repository, scope every runtime filesystem read, build with the complete production environment into an isolated distDir, and verify the exact artifact on a separate port before an atomic swap.

The deployed application now builds and serves correctly. The custom Kuku Yalanji and Mi'gmaq CPU model services remain intentionally unloaded; homepage translation uses the dictionary-guided prompt route.

2. User-visible and operator-visible symptoms

2.1 Original build behavior

2.2 Intermediate failures that were correctly rejected

2.3 Brief post-swap 502 period

After the first corrected release was swapped in, Caddy returned 502 for dynamic Next routes while static /docs/ continued to return 200. Inspection established:

Those investigation scans were stopped. The same direct root route then responded in 14 ms and the public root in 100 ms. No Caddy change was required. This was a storage-starvation readiness failure, not a port mismatch or a failed Next build.

3. Evidence-led root cause analysis

3.1 Forced Webpack on a Turbopack-default framework

Next 16 uses Turbopack by default for next dev and next build. The application instead declared:

{
  "dev": "next dev --webpack",
  "build": "next build --webpack"
}

The application also had a custom webpack(config) callback to provide aliases. Code search found no imports using those aliases. The actual shared package is imported as @mobtranslate/ui, which Next can transpile through transpilePackages.

The fix was:

{
  "dev": "next dev",
  "build": "next build"
}

and:

transpilePackages: ['@mobtranslate/ui'],
turbopack: {
  root: path.resolve(__dirname, '../..'),
},
outputFileTracingRoot: path.resolve(__dirname, '../..'),

This follows Next's documented Next 16 default, Turbopack root, package transpilation, and monorepo tracing paths.

3.2 Retained build trees inside Tailwind's discovery boundary

The repository kept rollback trees such as .next.pre-* and .next.release-* beside the live .next directory. They were neither source files nor valid inputs to a new build, but Tailwind v4's automatic source detection could still discover them. A live PostCSS worker had open files under a retained build's static tree.

Two changes close this path:

  1. Root .gitignore now includes .next.*/.
  2. Retained and failed builds now live under:
/mnt/donto-data/workspace/.build-archive/mobtranslate.com/YYYYMMDD/

The live source tree contains only .next plus the one isolated candidate being built or tested. A candidate is moved to .next at deployment; a superseded live tree is moved out to the archive.

3.3 Over-broad output-file tracing

Next's output-file tracing records files that each server route may need at runtime. Broad dynamic filesystem reads, computed paths spanning repository roots, and an evaluated require('sharp') caused traces to include unrelated project files and prior build output.

Measured before the fix:

Trace observation Count
Files in the sampled image-route trace 6,572
Files from the live .next tree 4,547

The runtime boundaries were corrected as follows:

The corrected output has 207 .nft.json route manifests. The audit found zero traces into a prior .next.* tree and zero unscoped project-root traces.

3.4 Production environment is part of the build contract

This application prerenders routes that initialize authentication and database-backed modules. A production build without the production environment is not representative and must not be deployed.

The accepted build loads /opt/mobtranslate/web.env without printing it. Custom model endpoint variables are then explicitly blanked so prerendering and runtime agree that CPU inference is paused.

Required secrets are never copied into this document, logs, model metadata, or public build output.

3.5 Disk contention was an independent operational cause

The fixed build path was run twice:

Build Compile TypeScript Static pages Wall time Result
Deployment candidate 37.1 s 77 s 25.0 s 173 s passed
Final analytics-clean build under contention 74 s 3.1 min 55 s 402 s passed

The code and route count were effectively unchanged except for the self-hosted analytics guard. The timing increase coincided with sustained 100% /dev/sdb utilization, so it must not be interpreted as a Turbopack regression.

Build monitoring must distinguish a silent but CPU-active TypeScript phase from a true hang. Check process state, CPU, output-tree modification time, and disk latency before terminating a quiet build.

4. Source changes

4.1 Build and workspace configuration

4.2 Filesystem and output-tracing boundaries

4.3 Live UX and self-hosting corrections

This list excludes unrelated pre-existing worktree changes.

5. Canonical build procedure

Run from the repository root. Choose a unique candidate name. Do not build into live .next.

cd /mnt/donto-data/workspace/mobtranslate.com

set -a
source /opt/mobtranslate/web.env
set +a

export MOBTRANSLATE_TRANSLATE_V2_ENDPOINT=
export MOBTRANSLATE_LABS_V2_ENDPOINT=
export MOBTRANSLATE_LABS_MIGMAQ_ENDPOINT=
export NEXT_DIST_DIR=.next.release-YYYYMMDDTHHMMZ
export NEXT_BUILD_CPUS=2

pnpm --filter web build

Mandatory build assertions:

test -s "apps/web/$NEXT_DIST_DIR/BUILD_ID"
test -d "apps/web/$NEXT_DIST_DIR/server"
test -d "apps/web/$NEXT_DIST_DIR/static"
cat "apps/web/$NEXT_DIST_DIR/BUILD_ID"

The worker cap is deliberate on this shared host. It limits peak pressure without disabling strict TypeScript or reducing the static-generation surface.

6. Candidate runtime procedure

Start the exact candidate on an unused port. The same NEXT_DIST_DIR must be present at runtime because Next reads it from next.config.js.

set -a
source /opt/mobtranslate/web.env
set +a

export MOBTRANSLATE_TRANSLATE_V2_ENDPOINT=
export MOBTRANSLATE_LABS_V2_ENDPOINT=
export MOBTRANSLATE_LABS_MIGMAQ_ENDPOINT=
export NEXT_DIST_DIR=.next.release-YYYYMMDDTHHMMZ
export PORT=3310

pnpm --filter web start

Verify real progress, not only process existence:

curl --fail --max-time 10 http://127.0.0.1:3310/api/version
curl --fail --max-time 30 http://127.0.0.1:3310/api/v1/models
curl --fail --max-time 30 http://127.0.0.1:3310/

Also test:

Stop the candidate process before moving its directory.

7. Atomic deployment and rollback

The production service runs with the default .next directory. Caddy proxies dynamic traffic to 127.0.0.1:3300.

Deployment sequence:

  1. Read and record both old and candidate BUILD_ID values.
  2. Stop mobtranslate-web.service.
  3. Move live .next to the external build archive; do not delete it.
  4. Move the verified candidate to .next.
  5. Start mobtranslate-web.service.
  6. Probe port 3300, then probe the public domain.
  7. Keep the rollback tree until a later explicitly verified retention cleanup.

If the candidate move fails, move the rollback tree back to .next before restarting the service. If runtime smoke fails after deployment, inspect service logs and storage pressure before deciding to roll back; a saturated disk can make a valid build appear unavailable.

Final deployment record:

Field Value
Deployed at 2026-07-14 13:55:44 UTC
New build ID ZS7-SuKDwqjovoz_tBnyE
Replaced build ID LUCL6-Xz-yvnyYPvmgJJa
Rollback tree .build-archive/mobtranslate.com/20260714/.next.rollback-LUCL6-Xz-yvnyYPvmgJJa-20260714T135544Z
Production upstream 127.0.0.1:3300
Service readiness 299 ms; zero restarts

8. Final verification matrix

8.1 Build and tests

Gate Result
Next version and bundler Next 16.2.9, Turbopack
Compile passed in 74 s
Strict TypeScript passed in 3.1 min under disk contention
Static generation 361/361 in 55 s
Total build wall time 402 s
Focused Vitest files 4 passed
Focused tests 13 passed
Route trace manifests 207
Unscoped prior-build traces 0

8.2 Live service and APIs

Gate Result
mobtranslate-web.service active, zero restarts
kuku-v2-infer.service inactive and disabled
mobtranslate-migmaq-v1-infer.service inactive and disabled
Ports 7955 and 8765 no listeners
GET / 200
Homepage dictionary-guided translation 200 with translation and gloss
GET /api/v1/models 200; two model families
Registered model releases 21
POST /api/labs/v2 intentional 503
POST /api/labs/migmaq intentional 503
valid POST /api/translate/v2 intentional 503 not_configured
GET /docs/ 200

8.3 Registry and benchmark evidence

Measure Count
Registry artifacts 147
Available artifact entries 129
Unique available URLs 125
Current v21.2 release artifacts 33
Current evaluation artifacts 16
Frozen benchmark rows per checkpoint 340
Three-checkpoint predictions 1,020
A40 run-to-run exact parity 1,020/1,020
A40-to-frozen-CPU exact parity 1,020/1,020

8.4 Browser verification

Real Chromium was used after the final public deployment:

Screenshots are retained in /opencode/logs/:

mobtranslate-live-final-1280-20260714.png
mobtranslate-live-final-mobile-menu-20260714.png

9. Monitoring and recurrence prevention

9.1 Before every build

9.2 During every build

9.3 Before every swap

9.4 After every swap

10. Known non-blocking notices

  1. Next 16 warns that middleware.ts is deprecated in favor of the proxy convention. It does not block this build, but the migration should be scheduled as a focused follow-up with auth and routing regression tests.
  2. Node reports that its SQLite API is experimental during prerender and runtime initialization. No SQLite failure was observed.
  3. The shared data disk remains a cross-application bottleneck. A future CI/build worker or cgroup/I/O scheduling policy would reduce variance, but the current build is reproducible and correct.

11. Official technical references

12. Audit artifacts

/opencode/logs/mobtranslate-build-turbopack-deploy-20260714T1326Z.log
/opencode/logs/mobtranslate-build-turbopack-deploy-20260714T1326Z.meta
/opencode/logs/mobtranslate-build-turbopack-final-20260714T1347Z.log
/opencode/logs/mobtranslate-build-turbopack-final-20260714T1347Z.meta
/opencode/logs/mobtranslate-live-final-1280-20260714.png
/opencode/logs/mobtranslate-live-final-mobile-menu-20260714.png

The source document is the canonical incident record. The rendered HTML and raw Markdown are published together by render-docs.sh; updating one without rerendering is incomplete.