Make the container actually deployable, publish to GHCR

The Docker setup built but never came up. Three separate blockers, each
found by running the full compose stack against a real Postgres:

- The runner stage hand-copied prisma/@prisma/dotenv, which misses the
  CLI's transitive deps (valibot) and crash-looped the entrypoint. A
  dedicated migrator stage now installs the CLI at the exact lockfile
  version with its real dependency tree.
- Docker sets HOSTNAME to the container id and Next's standalone server
  binds to it verbatim, so the app was unreachable on localhost inside
  the container and the healthcheck could never pass. Pinned to 0.0.0.0.
- openssl was missing from the build stages but present at runtime, so
  Prisma detected a different platform and tried to download a matching
  query engine at boot, as an unprivileged user, into a root-owned
  directory. Aligned openssl across every stage that touches Prisma.

Verified from a clean volume: migrations apply, both containers report
healthy, /api/health returns 200, and the app serves from the image.

Also:
- Publish multi-arch images to GHCR on push to main and on v* tags, with
  an SBOM and a signed provenance attestation.
- Default compose to the published image; add a build override for
  working on the app itself. Host port and image are configurable.
- Drop two unused dependencies: canvas (19MB of native node-canvas; the
  pipeline uses @napi-rs/canvas) and multer (routes use formData()).
- Add a root .gitignore. There wasn't one, so a real .env was one
  `git add .` away from being committed. Untrack .DS_Store.
- Rewrite the README around what the tool does and how to run it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-09-01 23:54:57 -05:00
1 parent 4130ce2e09
commit 7e7e4d9b1c
36 files changed
+2260 -1270

No files matched your search

+60 -1
View File
@@ -3,6 +3,12 @@
FROM node:20-bookworm-slim AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# openssl must be present in every stage that touches Prisma: the CLI picks its
# query-engine build from the detected libssl version, so a build stage without
# it would bake in an engine the runtime stage then tries to re-download.
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
FROM base AS deps
COPY package.json package-lock.json* ./
@@ -11,22 +17,75 @@ RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate the Prisma client (output: src/generated/prisma) before building.
RUN npx prisma generate
# Dummy build-time env only satisfies module-load guards during `next build`;
# real values are injected at runtime via compose. No secrets baked in.
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build"
ENV NEXTAUTH_SECRET="build"
ENV NEXTAUTH_URL="http://localhost:3000"
ENV GOOGLE_CLIENT_ID="build"
ENV GOOGLE_CLIENT_SECRET="build"
RUN npm run build
# The Prisma CLI is only needed to apply migrations at container start. Rather
# than drag the whole 600MB dev tree into the runtime image (or hand-pick
# packages and miss transitive deps like valibot), install just the CLI at the
# exact lockfile version into an isolated prefix, and copy that tree in.
FROM base AS migrator
COPY package-lock.json ./
RUN PRISMA_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/prisma'].version")" \
&& rm -f package-lock.json \
&& npm init -y > /dev/null \
&& npm install --no-audit --no-fund --omit=dev \
"prisma@${PRISMA_VERSION}" dotenv
# NOTE: the Studio dependency stack (effect, electric-sql, lodash, ...) looks
# prunable but is not — the CLI requires @prisma/studio-core/data/bff eagerly
# at startup, even for `migrate deploy`. Removing it breaks the entrypoint.
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV STORAGE_DIR=/app/storage
# Docker sets HOSTNAME to the container id, and Next's standalone server binds
# to it verbatim — which makes the app unreachable on localhost inside the
# container (breaking the healthcheck). Pin it to all interfaces.
ENV HOSTNAME=0.0.0.0
RUN groupadd --system --gid 1001 nodejs \
# openssl must match the build stages so Prisma resolves the same engine build.
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
# Next standalone server + assets
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Local Tesseract traineddata (no runtime CDN fetch)
COPY --from=builder /app/tessdata ./tessdata
# Prisma schema + migrations + CLI so the entrypoint can run `migrate deploy`.
# The CLI tree is merged into the standalone server's node_modules so that
# prisma.config.ts can resolve "prisma/config" and "dotenv/config" from /app.
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=migrator /app/node_modules ./node_modules
COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x ./docker-entrypoint.sh \
&& mkdir -p /app/storage \
&& chown -R nextjs:nodejs /app/storage /app/node_modules/@prisma
# Persist uploads + results outside the image layers
VOLUME ["/app/storage"]
USER nextjs
EXPOSE 3000
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["node", "server.js"]