b485e9870b
The far end of the Car Wash review outbox (wiki/concepts/vision-review-outbox.md): a small Fastify + SQLite service in the monorepo (shares the payload contract and the class vocabulary via @parking/shared), delivered to art-docker-station by its own stack so nothing booth-side lands there and nothing of it on a booth. - POST /ingest: bearer token per booth (constant-time), X-Booth-Id must match, multipart meta + JPEG (magic checked, 2 MB cap), meta validated against the contract, idempotent on the item id; crop stored at crops/<booth>/<item>.jpg on the volume + one items row. - /review + /api/*: the reviewer's screen served by the process (Basic auth, one login): one pending crop at a time, operator's pick and camera's pick beside it, one button/key per vocabulary class + unusable + skip; stats per booth and per hashed operator (agree / disagree / unusable — disagree = the reviewer's class is outside the operator's category). - GET /export/labels.csv: reviewed usable rows for training; formula-leading cells are neutralised (booth-supplied names). Crops stay on the volume for the trainer on the host. - Booth payload now carries operatorCategory.classes so the comparison needs no site setup. - Delivery: apps/collector/Dockerfile (monorepo context), docker-compose.collector.yml (bind to the overlay IP; commented `trainer` profile seam for the GPU), a third build step in build-images.yml, a `wash-collector` stack in komodo/resources.toml with one secret per booth referenced from both the collector's token list and the booth's own stack (park-2 lines templated, commented, DNS name for the URL). - Tests: app.test.ts (ingest ok/dup/refusals, review + stats + export, config). Image built and smoke-tested locally (health, ingest, duplicate, auth, verdict, export). Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
85 lines
4.3 KiB
Docker
85 lines
4.3 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
# Parking SERVER image: Fastify API + the bundled React SPA (one container serves both —
|
|
# offline-first single appliance). Build CONTEXT is the REPO ROOT (it's a pnpm/turbo
|
|
# monorepo). better-sqlite3 is a native module → build stage needs node-gyp toolchain,
|
|
# runtime needs libstdc++. Mirrors the house multi-stage pattern (cf. trm/processor).
|
|
# See wiki/decisions/container-deployment.md.
|
|
|
|
# ---- deps: cache-friendly pnpm fetch (only manifests change the layer) ----
|
|
FROM node:22-alpine AS deps
|
|
WORKDIR /app
|
|
RUN apk add --no-cache python3 make g++ # node-gyp for better-sqlite3
|
|
RUN corepack enable && corepack prepare pnpm@10.24.0 --activate
|
|
# Workspace manifests + lock first, so the fetch layer caches across source edits.
|
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
|
|
COPY apps/server/package.json apps/server/
|
|
COPY apps/web/package.json apps/web/
|
|
COPY apps/vision/package.json apps/vision/
|
|
COPY apps/collector/package.json apps/collector/
|
|
COPY packages/db/package.json packages/db/
|
|
COPY packages/devices/package.json packages/devices/
|
|
COPY packages/shared/package.json packages/shared/
|
|
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
pnpm fetch
|
|
|
|
# ---- build: install (offline from the fetched store) + turbo build everything ----
|
|
FROM deps AS build
|
|
ENV CI=true
|
|
COPY . .
|
|
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
pnpm install --frozen-lockfile --offline
|
|
# Force the SPA to use a SAME-ORIGIN (relative) API base for THIS image. Vite auto-loads
|
|
# apps/web/.env.production, which sets VITE_API_BASE=http://127.0.0.1:3000 for the TAURI
|
|
# DESKTOP build — but here Fastify serves the SPA same-origin, so an absolute base would
|
|
# make the browser hit 127.0.0.1:3000 cross-origin and fail CORS. `.env.production.local`
|
|
# has higher precedence than `.env.production`, so this empties it for the server image only.
|
|
RUN echo 'VITE_API_BASE=' > apps/web/.env.production.local
|
|
# Builds shared/db/devices, the server dist, AND the web SPA dist (apps/web/dist).
|
|
RUN pnpm turbo run build --filter=@parking/server --filter=@parking/web
|
|
# `pnpm deploy` produces a SELF-CONTAINED prod bundle for the server in /deploy: a hoisted
|
|
# node_modules with only @parking/server's prod deps (incl. the workspace packages' built
|
|
# dist + their native deps like better-sqlite3 — properly linked, unlike `prune` at root).
|
|
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
pnpm --filter=@parking/server --legacy deploy --prod /deploy
|
|
# The server's own dist + scripts (deploy copies the package's package.json + files, but we
|
|
# copy dist explicitly so the layout under /deploy is predictable). The web SPA + db
|
|
# migrations are copied in the runtime stage from their build locations.
|
|
|
|
# ---- runtime: slim, non-root ----
|
|
FROM node:22-alpine AS runtime
|
|
WORKDIR /app
|
|
# Set by CI to "<branch>-<short-sha>" (e.g. "stage-28bd838"), matching the same string used
|
|
# as the Komodo Stack's TAG (komodo/resources.toml) — so the version shown in the app is the
|
|
# same string an admin would look up there. Empty/absent on a local `docker build` (dev only).
|
|
ARG BUILD_VERSION=""
|
|
ENV BUILD_VERSION=$BUILD_VERSION
|
|
ENV NODE_ENV=production
|
|
RUN apk add --no-cache libstdc++ # better-sqlite3 native runtime
|
|
RUN addgroup -S app && adduser -S -G app app
|
|
|
|
# The self-contained deploy bundle: dist/ + a hoisted node_modules carrying the server's
|
|
# prod deps AND the workspace packages (@parking/db|devices|shared) with their built dist,
|
|
# the drizzle migrations, and the native better-sqlite3 binding. Single COPY — no scattered
|
|
# package dirs, no root node_modules.
|
|
COPY --from=build --chown=app:app /deploy ./
|
|
|
|
# The built SPA — served by Fastify static at WEB_DIST_DIR. (Not part of the server's deploy
|
|
# bundle, so copied from the web build output.)
|
|
COPY --from=build --chown=app:app /app/apps/web/dist ./web/dist
|
|
|
|
# DB lives on a mounted volume (never in the image). Default points at /data.
|
|
ENV DATABASE_URL=/data/parking.sqlite
|
|
ENV WEB_DIST_DIR=/app/web/dist
|
|
ENV HOST=0.0.0.0
|
|
ENV PORT=3000
|
|
RUN mkdir -p /data && chown app:app /data
|
|
VOLUME ["/data"]
|
|
|
|
USER app
|
|
EXPOSE 3000
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
CMD wget -qO- "http://localhost:${PORT:-3000}/health" >/dev/null 2>&1 || exit 1
|
|
|
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
CMD ["node", "dist/index.js"]
|