Files
parking_solution/apps/server/Dockerfile
T
julian 77b2acb1ca fix(docker): SPA must use same-origin API base in the server image (CORS)
apps/web/.env.production sets VITE_API_BASE=http://127.0.0.1:3000 for the TAURI
desktop build (which loads from tauri://localhost and needs an absolute backend
origin). But Vite auto-loads .env.production for ANY `vite build`, so the server
image baked 127.0.0.1:3000 into the browser bundle — loading the UI from a real
host (e.g. http://parksystems.msai.al) then made the browser call 127.0.0.1:3000
cross-origin and fail the Same-Origin Policy on /api/auth/login.

Fix: the server Dockerfile writes apps/web/.env.production.local with an empty
VITE_API_BASE before the web build (.local has higher Vite precedence), so the SPA
served by Fastify stays relative/same-origin (/api/...). The desktop build is
unaffected (it doesn't use this Dockerfile). Verified: 127.0.0.1:3000 no longer in
the built bundle; /api/auth/login is relative.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-23 18:58:53 +02:00

79 lines
4.0 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 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
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"]