Files
NextChair/Dockerfile
T
Stefan Lazic 9e39908ea7 Post Phase 8 - Fixes 1
Delivered:

Super Administrator — env-var provisioned, no StaffProfile at all (structural exclusion from Team/barber-picker/appointments/staff-totals, not a filter). hasPermission short-circuit + new getActingBusinessContext() helper retrofitted across 16 owner pages so it can actually manage the business.
Media Manager — one upload/library/external-URL modal + field picker, backing every image field app-wide. Local-disk storage + Docker volume, SVG rejected, size-capped.
Welcome backgrounds — desktop/mobile images with CSS breakpoint swap.
Business Branding settings — logo/cover/welcome/favicon/icon/colours all editable post-install; cover image finally rendered (was dead config).
Service descriptions — now surfaced on Home and in the booking flow.
Loyalty card — live preview, ColorPicker/MediaPicker/IconPicker, flip removed for a static premium-styled card, Loyalty added to bottom nav. Fixed a real pre-existing bug where the card's configured colour/image/icon were stored but never actually rendered.
Home Screen Redesign — hero gallery + sticky Services/Portfolio/Reviews/Details tabs, backed by a new unified gallery model/service (no hero/portfolio duplication). Details tab surfaces social links and a map that were never shown before.
Google Reviews — real sync pipeline (24h cron, CRON_SECRET-gated, mirrors the existing notification cron exactly), DB-only customer reads, owner hide/unhide + policy management page.
Architecture cleanup — SettingsPageShell extracted from 6 duplicated page wrappers; fixed a second pre-existing bug where branding image fields could never actually be cleared once set.
Verification: 418 tests passing (236 unit + 182 integration, all against a real MySQL database), full typecheck/lint clean across src, and a real Docker build+run cycle — which caught and fixed a genuine bug (scripts/ wasn't copied into the production image, so Super Admin sync would have crashed on every real deployment).
2026-08-04 17:10:30 +01:00

75 lines
3.6 KiB
Docker

# Multi-stage build. NextChair is the internal project/image name only —
# see docs/phase-8-summary.md's white-label section for why that's fine
# here (Docker/package names are explicitly "internal and acceptable")
# even though the running application never shows that name to a user.
#
# Prisma 7 uses driver adapters (@prisma/adapter-mariadb), not a native
# Rust query-engine binary — the generated client is pure JS, so this
# image needs no special native-binary handling the way older Prisma
# versions on Alpine sometimes did.
#
# `output: "standalone"` in next.config.ts was deliberately NOT enabled:
# this image runs `prisma migrate deploy` (and, for the optional seed
# service, `tsx prisma/seed.ts`) at container start, both of which need
# the full `prisma`/`tsx` CLIs and their dependencies present — a pruned
# standalone bundle would need those re-added anyway, so a single
# `npm ci` (full dependencies, including the small set of dev tools this
# image actually runs) is simpler and no less correct. See
# docs/phase-8-summary.md, "Docker image quality" for this trade-off.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# DATABASE_URL isn't needed to generate the client (no DB connection at
# generate time) — only real values needed at runtime, never baked in here.
RUN npx prisma generate
# `next build`'s page-data collection step imports every route module
# (including /api/health) to inspect it — that pulls in
# src/lib/db/prisma.ts, which requires DATABASE_URL just to construct its
# connection-pool config object, even though nothing actually connects at
# build time. This placeholder only exists to satisfy that check; the
# real value is supplied at container runtime via env_file (see
# docker-compose.local.yaml) and this one is never used to connect to
# anything or baked into a layer that matters.
ENV DATABASE_URL="mysql://build:build@localhost:3306/build"
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Non-root — see the brief's "Docker image quality: non-root user."
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextchair
COPY --from=builder --chown=nextchair:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextchair:nodejs /app/.next ./.next
COPY --from=builder --chown=nextchair:nodejs /app/public ./public
COPY --from=builder --chown=nextchair:nodejs /app/generated ./generated
COPY --from=builder --chown=nextchair:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextchair:nodejs /app/src ./src
COPY --from=builder --chown=nextchair:nodejs /app/scripts ./scripts
COPY --from=builder --chown=nextchair:nodejs /app/package.json ./package.json
COPY --from=builder --chown=nextchair:nodejs /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder --chown=nextchair:nodejs /app/next.config.ts ./next.config.ts
COPY --from=builder --chown=nextchair:nodejs /app/tsconfig.json ./tsconfig.json
COPY --chown=nextchair:nodejs docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh
# Media Manager uploads (Phase 9) — public/media is a named volume mount
# point (see docker-compose.local.yaml). Creating it here, owned by the
# non-root user, before that volume is first attached means Docker
# initializes the volume with this ownership rather than root's — see
# src/lib/media/media-storage.ts.
RUN mkdir -p ./public/media && chown nextchair:nodejs ./public/media
USER nextchair
EXPOSE 3000
ENV PORT=3000
ENTRYPOINT ["./docker/entrypoint.sh"]