Add Docker packaging for local functional review (api + web)
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:
parent
42d094764f
commit
64a19b5112
6 changed files with 110 additions and 0 deletions
11
.dockerignore
Normal file
11
.dockerignore
Normal 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
|
||||||
|
|
@ -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
36
apps/api/Dockerfile
Normal 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
13
apps/web/Dockerfile
Normal 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
11
apps/web/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue