From cbea5e8fa49ec479b2482a538d8bcd4329fa7202 Mon Sep 17 00:00:00 2001 From: krisf Date: Sat, 15 Aug 2026 18:11:41 -0400 Subject: [PATCH] deploy: Dockerfile, entrypoint, standalone output, dockerignore --- .dockerignore | 15 +++++++++++ Dockerfile | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ entrypoint.sh | 27 ++++++++++++++++++++ next.config.mjs | 2 ++ 4 files changed, 110 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..57233fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +.next +.git +.gitignore +env.d.ts +env.example +prisma/dev.db +prisma/fresh.db* +prisma/dev.db-journal +prisma/dev.db-shm +prisma/dev.db-wal +*.tar.gz +*.log +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f405c7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +# MapleBrief — production image +# Multi-stage: deps -> build -> minimal runner +# (Debian base: Prisma engines need glibc; Alpine breaks the schema/query engine.) +# +# Build args / secrets: +# NEXT_PUBLIC_ADSENSE_CLIENT_ID — AdSense client id baked into the build +# (empty -> ads hidden, app still works) +# Runtime env: +# ADMIN_API_KEY — admin/auth key (fail-closed 401 if empty) +# OLLAMA_BASE / provider — LLM endpoints (or configure in admin UI) +# DATABASE_URL defaults to file:/data/dev.db (volume-mounted) + +# Optional: bake the real AdSense client id at build time (empty -> ads hidden) +ARG NEXT_PUBLIC_ADSENSE_CLIENT_ID="" + +FROM node:22-slim AS deps +WORKDIR /app +RUN apt-get update -qq && apt-get install -yq --no-install-recommends openssl ca-certificates > /dev/null && rm -rf /var/lib/apt/lists/* +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +FROM node:22-slim AS build +WORKDIR /app +# inherit the global value (set via --build-arg); empty -> placeholder ads +ARG NEXT_PUBLIC_ADSENSE_CLIENT_ID +RUN apt-get update -qq && apt-get install -yq --no-install-recommends openssl ca-certificates > /dev/null && rm -rf /var/lib/apt/lists/* +# Absolute path avoids Prisma resolving relative SQLite URLs against the +# schema dir (which would double up into prisma/prisma/...). +ENV DATABASE_URL="file:/app/fresh.db" +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# Verify migrations apply to a fresh DB, then stage the migrated empty DB at +# the exact path the runner copies. +RUN npx prisma generate \ + && npx prisma migrate deploy \ + && cp /app/fresh.db prisma/fresh.db \ + && NEXT_PUBLIC_SITE_URL="https://news.krisforbes.ca" \ + NEXT_PUBLIC_SITE_NAME="MapleBrief" \ + NEXT_PUBLIC_SITE_DESCRIPTION="Synthesized Canadian news briefings" \ + NEXT_PUBLIC_ADSENSE_CLIENT_ID="${NEXT_PUBLIC_ADSENSE_CLIENT_ID}" \ + NEXT_PUBLIC_SHOW_SIDEBAR_ADS="false" \ + npm run build \ + && rm -f /app/fresh.db prisma/fresh.db-journal prisma/fresh.db-shm +# (prisma/fresh.db kept above for the runner stage copy) + +FROM node:22-slim AS runner +WORKDIR /app +RUN apt-get update -qq && apt-get install -yq --no-install-recommends curl > /dev/null && rm -rf /var/lib/apt/lists/* +# runtime user (guard: node slim images don't always ship one) +RUN id app 2>/dev/null || useradd -m -s /bin/sh app +# pre-create the data dir (volume replaces it at runtime) with app ownership +RUN mkdir -p /data && chown -R app:app /data +USER app +# Next standalone server +COPY --chown=app:app --from=build /app/.next/standalone ./ +# static chunks + public assets (standalone does not auto-copy these) +COPY --chown=app:app --from=build /app/.next/static ./.next/static +COPY --chown=app:app --from=build /app/public ./public +# pre-migrated clean database snapshot (entrypoint copies it to /data on first boot) +COPY --chown=app:app --from=build /app/prisma/fresh.db ./seed-dev.db +COPY --chown=app:app entrypoint.sh ./entrypoint.sh +RUN chmod +x entrypoint.sh +ENV PORT=3000 +ENV DATABASE_URL="file:/data/dev.db" +EXPOSE 3000 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..86fbac5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# MapleBrief entrypoint +# 1) First boot: seed /data with the clean migrated DB + write privileged key if provided +# 2) Start Next standalone server +set -e +cd /app + +DB="${DATABASE_URL#file:}" +[ -n "$DB" ] || DB=/data/dev.db + +# Export an absolute DATABASE_URL for the Node/Prisma process (relative +# paths resolve against the schema dir and can point at the wrong file). +case "$DB" in + /*) ;; + *) DB="/$DB" ;; +esac +export DATABASE_URL="file:$DB" + +if [ ! -f "$DB" ]; then + # create parent dir (volume may be fresh) + DIR="$(dirname "$DB")" + mkdir -p "$DIR" + cp ./seed-dev.db "$DB" + echo "[entrypoint] initialized $DB from seed snapshot" +fi + +exec node server.js diff --git a/next.config.mjs b/next.config.mjs index e76926a..eed4455 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,8 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + // Minimal Docker image: the runner stage copies .next/standalone. + output: 'standalone', // RSS/Google AdSense scripts are injected safely by our own client // components (see src/components/ads). Keep lint from failing the build // on un-stulated style warnings while keeping full type checking on.