diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 176a9e73..38e1ac2e 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -32,6 +32,9 @@ const nextConfig: NextConfig = { return [host, host ? `*.${apex(host)}` : "", "localhost", "127.0.0.1"].filter(Boolean); })(), experimental: { + turbopackMemoryLimit: process.env.DOABLE_BUILD_MEMORY_MB + ? Number(process.env.DOABLE_BUILD_MEMORY_MB) * 1024 * 1024 + : undefined, serverActions: { bodySizeLimit: "2mb", }, diff --git a/deployment/docker/Caddyfile b/deployment/docker/Caddyfile index 7cd4bfe1..a70fc236 100644 --- a/deployment/docker/Caddyfile +++ b/deployment/docker/Caddyfile @@ -36,6 +36,19 @@ {$DOABLE_SITE:localhost} { tls {$DOABLE_TLS:internal} + # Compress preview modules and static assets; leave SSE and API streams alone. + @compressible_assets path /_next/static/* /preview/* /api/preview/* /brand/* /sites/* + encode @compressible_assets zstd gzip { + match { + header Content-Type text/javascript* + header Content-Type application/javascript* + header Content-Type text/css* + header Content-Type text/html* + header Content-Type application/json* + header Content-Type image/svg+xml* + } + } + # ─── OTLP browser tracing exporter → Next.js proxy (web container) ── # apps/web/src/app/api/otlp/[...path]/route.ts forwards to the api # container's /internal/otlp/* receiver. MUST come before the diff --git a/deployment/docker/Dockerfile b/deployment/docker/Dockerfile index 0a99397a..364c87fd 100644 --- a/deployment/docker/Dockerfile +++ b/deployment/docker/Dockerfile @@ -90,7 +90,17 @@ COPY . . # Build packages that compile cleanly (docore, dovault, web) RUN pnpm --filter=docore run build || true RUN pnpm --filter=dovault run build || true -RUN pnpm --filter=@doable/web run build +# Only the web image needs Next.js output. API/WS/migrate copy the shared +# packages without recompiling the frontend on every backend source change. +FROM build AS web-build + +# Optional budget for source builds on memory-constrained hosts. +# Leave empty to retain the existing Node/Next.js defaults. +ARG WEB_BUILD_MEMORY_MB= +RUN if [ -n "$WEB_BUILD_MEMORY_MB" ]; then \ + NODE_OPTIONS="--max-old-space-size=$WEB_BUILD_MEMORY_MB" \ + DOABLE_BUILD_MEMORY_MB="$WEB_BUILD_MEMORY_MB" pnpm --filter=@doable/web run build; \ + else pnpm --filter=@doable/web run build; fi # --- API service --- FROM base AS api @@ -100,9 +110,7 @@ WORKDIR /app # libs Chrome crashes at first thumbnail attempt with # `libnspr4.so: cannot open shared object file`. Same dep list as # deployment/server-setup.sh:298-322 so docker + bare-metal stay in -# lockstep. The bundled Chromium itself downloads in the next layer -# (PUPPETEER_SKIP_CHROMIUM_DOWNLOAD is intentionally unset for the api -# stage). +# lockstep. The distro Chromium below is also the runtime executable. # # `chromium` is the distro-native browser we point Puppeteer at via # PUPPETEER_EXECUTABLE_PATH below. On Apple Silicon hosts running native @@ -155,29 +163,14 @@ RUN mkdir -p /app/services/api/projects /app/services/api/thumbnails /data/sites && ln -sf services/api/thumbnails /app/thumbnails \ && chown -h node:node /app/projects /app/thumbnails -# Download the puppeteer-bundled Chrome into the image so first-thumbnail -# capture doesn't need network. Cached layer; runs once per build. -# -# R14 BUG-DOCKER-PUPPETEER: PUPPETEER_CACHE_DIR was previously /app/.puppeteer-cache -# but the WORKDIR /app dir itself is owned by root (only the COPY'd contents -# get chowned to node via --chown=node:node). USER node then hit EACCES on -# mkdir of the cache dir and the build logged "Chrome install/smoke test -# failed — thumbnails will be unavailable" on every clean build. Move the -# cache to node's home (/home/node/.cache/puppeteer), which the node:22-slim -# base image already creates and owns. Runtime container inherits the same -# path because the api service also runs as USER node. +# Use the distro Chromium installed above, matching the runtime executable. +# Avoid a redundant network download of Puppeteer's fallback browser on each +# source update. Fail the build if the configured browser is not executable. USER node ENV PUPPETEER_CACHE_DIR=/home/node/.cache/puppeteer -# Point Puppeteer at the apt-installed `chromium` so puppeteer.launch() -# always uses a native-arch binary (see the chrome-arch comment above the -# apt install block). The bundled `puppeteer browsers install chrome` -# step below still runs as a fallback / cache-warmer — if PUPPETEER_EXECUTABLE_PATH -# is ever unset at runtime, the bundled binary takes over. ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium RUN mkdir -p "$PUPPETEER_CACHE_DIR" \ - && cd services/api && pnpm exec puppeteer browsers install chrome \ - && /usr/bin/chromium --headless=new --no-sandbox --disable-gpu --version \ - || (echo "[api-image] Chromium smoke test failed — thumbnails will be unavailable" && true) + && /usr/bin/chromium --headless=new --no-sandbox --disable-gpu --version EXPOSE 4000 ENTRYPOINT ["tmux-entrypoint"] @@ -202,8 +195,8 @@ CMD ["ws", "npx", "tsx", "services/ws/src/index.ts"] # --- Web (Next.js standalone) --- FROM base AS web WORKDIR /app -COPY --from=build --chown=node:node /app/apps/web/.next/standalone ./ -COPY --from=build --chown=node:node /app/apps/web/.next/static ./apps/web/.next/static +COPY --from=web-build --chown=node:node /app/apps/web/.next/standalone ./ +COPY --from=web-build --chown=node:node /app/apps/web/.next/static ./apps/web/.next/static # Runtime placeholder rewriter — replaces __DOABLE_*_URL__ placeholders # baked at build time with the operator's actual NEXT_PUBLIC_* values from # container env. Lets one image work for any deployment URL. diff --git a/deployment/docker/README.md b/deployment/docker/README.md index 49199011..d92c70f5 100644 --- a/deployment/docker/README.md +++ b/deployment/docker/README.md @@ -397,3 +397,22 @@ rm -f deployment/docker/.env rm -rf deployment/docker/certs/*.pem ./deployment/docker/setup.sh ``` + +### Source-build resource usage + +`WEB_BUILD_MEMORY_MB` is an optional Docker build argument that bounds both the +Node.js heap and the Next.js Turbopack memory cache for the web compilation: + +```sh +docker compose --env-file deployment/docker/.env -f deployment/docker/docker-compose.yml build --build-arg WEB_BUILD_MEMORY_MB=512 web +``` + +The default is unset, preserving the existing compiler defaults. A small budget +can increase build time or be insufficient for a particular build; this is not a +container memory limit. Provision build capacity separately from running apps. + +The API's npm download cache is kept in the `npm_cache` named volume so rebuilding +the API does not discard cached project dependencies. Project source remains in +`api_projects`. Caddy compresses supported preview/static asset responses; AI +SSE endpoints are outside the compression path matcher and `text/event-stream` +is excluded from its response MIME matcher. diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml index 1d36cf1c..a32208af 100644 --- a/deployment/docker/docker-compose.yml +++ b/deployment/docker/docker-compose.yml @@ -227,6 +227,8 @@ services: volumes: - api_projects:/app/services/api/projects - api_thumbnails:/app/services/api/thumbnails + # Preserve dependency downloads across API updates; never store project source here. + - npm_cache:/home/node/.npm # Published static sites — written here by the deploy adapters, served # read-only by caddy from /srv/sites (path topology). - doable_sites:/data/sites @@ -378,6 +380,7 @@ volumes: postgres_data: api_projects: api_thumbnails: + npm_cache: ws_projects: caddy_data: caddy_config: