From 64a19b5112c613c04a9d4fb95e94cd2f7cbc4172 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 16 Aug 2026 13:53:13 +0200 Subject: [PATCH] Add Docker packaging for local functional review (api + web) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apps/api/Dockerfile: multi-stage build on node:22-slim (not alpine — avoids musl-vs-glibc native binding surprises for argon2/Prisma's engine binaries; same base image family for build and runtime stages keeps "native" binaries compatible across stages). Runtime stage copies the monorepo structure as-is rather than flattening to a single package, so pnpm's symlinked node_modules stay valid. Container runs `prisma migrate deploy` on startup before starting the server, so the review environment's schema is always in sync automatically. Installs openssl explicitly in the base image: without it, Prisma can't detect the right engine binary and silently defaults to a guess that may not match what's actually on the image — caught by checking the build log, not just a successful build. apps/web/Dockerfile: builds with Vite, serves the static output via nginx (not a Node static server) — avoids the devDependency problem of needing `vite preview` in a --prod-deployed image, and is the more standard way to serve a built SPA. nginx.conf has an SPA fallback (try_files ... /index.html) ready for when client-side routing lands. docker-compose.yml: adds `api` and `web` services alongside the existing `postgres`. api's DATABASE_URL targets the `postgres` service name over the compose network (not localhost/POSTGRES_PORT, which is only the host-side mapping). Both new services require JWT_SECRET/ ports via env vars with no defaults, consistent with the project's existing no-hardcoded-credentials rule. .dockerignore added — without it, the Windows-built node_modules (with Windows-specific native binaries) would get copied into the Linux build context. Verified: full build (api + web images), `docker compose up -d` brought up all three containers, curled /health and the web root, ran a real signup through the containerized stack end-to-end. --- .dockerignore | 11 +++++++++++ .env.example | 9 +++++++++ apps/api/Dockerfile | 36 ++++++++++++++++++++++++++++++++++++ apps/web/Dockerfile | 13 +++++++++++++ apps/web/nginx.conf | 11 +++++++++++ docker-compose.yml | 30 ++++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 .dockerignore create mode 100644 apps/api/Dockerfile create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..31b363a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +**/node_modules +**/dist +.git +**/.env +**/.env.* +!**/.env.example +*.log +.pnpm-store +apps/web/cypress/videos +apps/web/cypress/screenshots +apps/web/cypress/downloads diff --git a/.env.example b/.env.example index fdcc5a0..7639ced 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,12 @@ POSTGRES_USER=changeme POSTGRES_PASSWORD=changeme POSTGRES_DB=batchcooking POSTGRES_PORT=5432 + +# Used by docker-compose.yml's "api" service (Docker-only — the native +# `pnpm dev:api` workflow reads apps/api/.env instead, set both when using +# both workflows). Required, no default on purpose — generate your own. +JWT_SECRET=changeme-generate-a-real-random-secret-at-least-32-chars + +# Optional — host ports for the Docker review stack (docker-compose.yml) +# API_PORT=3000 +# WEB_PORT=8080 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..c51a35a --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,36 @@ +# Debian-based (not alpine) on purpose: avoids musl-vs-glibc native binding +# surprises for argon2/Prisma's engine binaries. Same base image family for +# build and runtime stages, so "native" binaries built in `build` are +# guaranteed compatible with `runtime`. +FROM node:22-slim AS base +# Prisma's query engine needs OpenSSL to be present to detect the right +# binary target; without it, it silently defaults to a guess (openssl-1.1.x) +# that may not match what's actually on the image and fail at runtime. +RUN apt-get update && apt-get install -y --no-install-recommends openssl && rm -rf /var/lib/apt/lists/* +RUN corepack enable +WORKDIR /repo + +FROM base AS build +COPY . . +RUN pnpm install --frozen-lockfile +RUN pnpm --filter api build + +# Copies the monorepo structure as-is (not a flattened single package) so +# pnpm's symlinked node_modules (root node_modules/.pnpm <- apps/api/node_modules) +# stay valid — paths must match exactly between build and runtime stages. +FROM base AS runtime +ENV NODE_ENV=production +COPY --from=build /repo/node_modules ./node_modules +COPY --from=build /repo/package.json ./package.json +COPY --from=build /repo/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=build /repo/packages/shared ./packages/shared +COPY --from=build /repo/apps/api/node_modules ./apps/api/node_modules +COPY --from=build /repo/apps/api/dist ./apps/api/dist +COPY --from=build /repo/apps/api/prisma ./apps/api/prisma +COPY --from=build /repo/apps/api/package.json ./apps/api/package.json + +WORKDIR /repo/apps/api +EXPOSE 3000 +# Applies pending migrations before starting — keeps the review environment's +# schema in sync automatically, no manual step needed. +CMD ["sh", "-c", "node_modules/.bin/prisma migrate deploy && node dist/server.js"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..1e8278f --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22-slim AS base +RUN corepack enable +WORKDIR /repo + +FROM base AS build +COPY . . +RUN pnpm install --frozen-lockfile +RUN pnpm --filter web build + +FROM nginx:alpine AS runtime +COPY --from=build /repo/apps/web/dist /usr/share/nginx/html +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..8103e43 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA fallback — needed once client-side routing (react-router) lands. + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index e6ea603..d37f92a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,5 +19,35 @@ services: timeout: 5s retries: 5 + api: + build: + context: . + dockerfile: apps/api/Dockerfile + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3000 + # Uses the "postgres" service name, not localhost/POSTGRES_PORT — + # container-to-container traffic stays on the compose network and + # always targets Postgres's internal port (5432). + DATABASE_URL: "postgresql://${POSTGRES_USER:?set POSTGRES_USER in .env}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/${POSTGRES_DB:?set POSTGRES_DB in .env}?schema=public" + JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env} + CORS_ORIGIN: "http://localhost:${WEB_PORT:-8080}" + ports: + - "${API_PORT:-3000}:3000" + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + restart: unless-stopped + depends_on: + - api + ports: + - "${WEB_PORT:-8080}:80" + volumes: postgres_data: