# Nextcraft — single-port docker image (v0.8 P2, REQ-8-002, R-16..R-19) # # Topology: uvicorn :8420 serves BOTH the API (/v1/*, /health) and the # exported web UI (everything else) — the same single-port production # shape as `nextcraft deploy` (D-039). Deps are pre-loaded in image # layers; runtime is unprivileged by default (honest sandbox degrade: # ai_service/sandbox/capability.py probes + api/sandboxes.py 503s with # remediation — D-068/G-208). # # Build context is the repo root. Build args: # NEXTCRAFT_VERSION — stamped into the embedded CLI (`--version`) # # Stages: # 1. web-export — node:24-bookworm: pnpm workspace install, static # export (NEXT_PUBLIC_AI_SERVICE_URL=self), SEA CLI binary build # 2. venv — python:3.11-bookworm: production deps only (no [dev]) # 3. runtime — python:3.11-slim-bookworm: non-root, venv + package + # export + CLI, /health healthcheck, state volume # -- stage 1: web export + CLI binary --------------------------------------- FROM node:24-bookworm AS web-export ARG NEXTCRAFT_VERSION=0.0.0-dev WORKDIR /repo # Toolchain: corepack ships with node 24 (deprecated but present). RUN corepack enable pnpm # Workspace manifests first (layer-cacheable install). COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc tsconfig.json ./ COPY apps/web/package.json apps/web/ COPY apps/cli/package.json apps/cli/ COPY packages/types/package.json packages/types/ COPY packages/ui/package.json packages/ui/ COPY packages/mock-data/package.json packages/mock-data/ RUN pnpm install --frozen-lockfile # Sources (web build needs all workspaces; CLI build needs its tree). COPY apps ./apps COPY packages ./packages # Static export (same command as the deploy path): the export lands in # apps/web/out and is served same-origin by uvicorn. ENV NEXT_PUBLIC_AI_SERVICE_URL=self RUN pnpm --filter web build # Embedded CLI (D-066): doctor/verify/guide work in-container; the daemon # commands refuse honestly (see apps/cli/src — NC_CONTAINER detection). RUN pnpm --filter @nextcraft/cli build:binary "$NEXTCRAFT_VERSION" # -- stage 2: python venv ---------------------------------------------------- FROM python:3.11-bookworm AS venv WORKDIR /repo # The full source tree installs the package (bootstrap.sh's mechanism # without [dev]: production deps only — the image runs the app, it never # lints/tests it; the runner lanes own that). COPY apps/ai-service /repo/apps/ai-service RUN python3 -m venv /opt/venv && \ /opt/venv/bin/pip install --no-cache-dir --upgrade pip && \ /opt/venv/bin/pip install --no-cache-dir /repo/apps/ai-service # -- stage 3: runtime -------------------------------------------------------- FROM python:3.11-slim-bookworm AS runtime WORKDIR /opt/nextcraft # Non-root user; home carries the state volume (~/.nextcraft semantics — # config.py anchors db/sandboxes under $HOME/.nextcraft, R-17). RUN useradd --create-home --uid 10001 nextcraft # venv from stage 2 (production deps only). COPY --from=venv /opt/venv /opt/venv ENV PATH=/opt/venv/bin:$PATH # The installed ai_service package is IN the venv's site-packages; the # source tree rides along for transparency (PYTHONPATH prefers the # installed package). COPY --from=venv /repo/apps/ai-service /opt/nextcraft/ai-service-src RUN rm -rf /opt/nextcraft/ai-service-src/.venv # Web export from stage 1. COPY --from=web-export /repo/apps/web/out /opt/nextcraft/web-out ENV AI_WEB_STATIC_DIR=/opt/nextcraft/web-out # Image marker: NC_CONTAINER=1 (context for the CLI container-mode # branches and the degrade boot notice; the capability PROBE — not this # marker — decides degrade, D-068). ENV NC_CONTAINER=1 # Embedded CLI (D-066). COPY --from=web-export /repo/apps/cli/dist/nextcraft-linux-x64 /usr/local/bin/nextcraft RUN chmod 0755 /usr/local/bin/nextcraft # App dir + state: db/sandboxes under /home/nextcraft/.nextcraft (R-17). # The state root is pre-created with correct ownership BEFORE the VOLUME # declaration — an anonymous volume copies the image's directory CONTENTS # and ownership at first use, so the app (uid 10001) can mkdir its data/ # and sandboxes/ subdirs on a clean boot (rc5 smoke-gate finding: the # bare useradd home did not carry a writable .nextcraft). RUN mkdir -p /home/nextcraft/.nextcraft/data \ && chown -R nextcraft:nextcraft /opt/nextcraft /home/nextcraft/.nextcraft WORKDIR /opt/nextcraft/ai-service-src USER nextcraft ENV AI_HOST=0.0.0.0 AI_PORT=8420 EXPOSE 8420 # slim has no curl — the healthcheck speaks python (stdlib urllib). HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD ["python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8420/health', timeout=4).status == 200 else 1)"] VOLUME ["/home/nextcraft/.nextcraft"] CMD ["python3", "-m", "uvicorn", "ai_service.main:app", "--host", "0.0.0.0", "--port", "8420"]