Add Docker packaging for local functional review (api + web) (#5)

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.
This commit is contained in:
kyuno053 2026-08-16 14:13:51 +02:00 committed by GitHub
parent 42d094764f
commit bc582b75d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 110 additions and 0 deletions

11
.dockerignore Normal file
View file

@ -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

View file

@ -5,3 +5,12 @@ POSTGRES_USER=changeme
POSTGRES_PASSWORD=changeme POSTGRES_PASSWORD=changeme
POSTGRES_DB=batchcooking POSTGRES_DB=batchcooking
POSTGRES_PORT=5432 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

36
apps/api/Dockerfile Normal file
View file

@ -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"]

13
apps/web/Dockerfile Normal file
View file

@ -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

11
apps/web/nginx.conf Normal file
View file

@ -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;
}
}

View file

@ -19,5 +19,35 @@ services:
timeout: 5s timeout: 5s
retries: 5 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: volumes:
postgres_data: postgres_data: