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:
parent
42d094764f
commit
bc582b75d6
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_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
|
||||
|
|
|
|||
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
|
||||
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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue