Files
vyndr/Dockerfile
T
builtbykev c2c43cdc92 Task A — make the container backup-capable + validated dump + mechanism fingerprint
SUPABASE_DB_URL is set in Coolify on the API service, and this WSL2 box can't
reach db.<ref>.supabase.co — so the backup runs INSIDE the API container, which
has the env + Supabase network. Made that real:
- Dockerfile: install postgresql-client (pg_dump/pg_restore) + rsync + bash in
  the runner image.
- backup-db.sh: added an integrity fingerprint on every run — pg_restore --list
  must parse the archive AND find ledger_entries, else the run FAILS + pages
  (stronger than the size check; catches a corrupt/structureless dump).
- BACKUP-RUNBOOK.md: rewritten for the container-exec reality — host cron does
  `docker exec <api> sh /app/scripts/backup-db.sh` (inherits env + network +
  pg_dump), or a Coolify Scheduled Task. Full restore-fingerprint steps included.

MECHANISM FINGERPRINT (run locally, docker + pg16): seeded a ledger_entries
table (137 rows) → ran backup-db.sh (dump + validate: 22 archive objects,
ledger_entries present) → pg_restore into a scratch DB → 137 rows restored,
exact match. The dump/validate/restore path is proven end-to-end; it's the same
pg_dump/pg_restore that run in the container against Supabase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 23:08:31 -04:00

79 lines
2.7 KiB
Docker

# syntax=docker/dockerfile:1.6
#
# VYNDR Express backend (port 3001).
#
# Multi-stage build:
# 1. deps — install production deps with a clean lockfile
# 2. runner — copy src/, poller/, scripts/, node_modules and start
#
# The Next.js frontend ships in a separate image (web/Dockerfile). PM2
# pollers run INSIDE this image and are auto-started by
# scripts/docker-entrypoint.sh so a Coolify redeploy reseeds them on
# every container start.
#
# Build: docker build -t vyndr-api .
# Run: docker run -p 3001:3001 --env-file .env vyndr-api
# --- deps stage ---
FROM node:20-alpine AS deps
WORKDIR /app
# package-lock.json is the source of truth — npm ci reproduces it exactly.
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund
# --- runner stage ---
FROM node:20-alpine AS runner
WORKDIR /app
# curl — /api/health smoke check (Coolify HEALTHCHECK).
# postgresql-client (pg_dump/pg_restore) + rsync + bash — the nightly DB backup
# (scripts/backup-db.sh) runs INSIDE this container, where SUPABASE_DB_URL and
# the Supabase network are available. See docs/BACKUP-RUNBOOK.md.
RUN apk add --no-cache curl tini bash postgresql-client rsync
# PM2 is installed globally so the entrypoint can call `pm2 start` to
# boot all three pollers (NBA / WNBA / MLB) alongside the Express API.
RUN npm install -g pm2@latest --no-audit --no-fund
ENV NODE_ENV=production \
PORT=3001 \
PM2_HOME=/app/.pm2
# Non-root user — the container should never run as uid 0 even if the
# host accidentally maps a privileged port.
RUN addgroup -S vyndr && adduser -S vyndr -G vyndr
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json ./
COPY src ./src
COPY poller ./poller
COPY scripts ./scripts
COPY supabase ./supabase
# Session 65 (P0) — content/ holds the Stark-line library + seed articles.
# It was NOT copied, so mediaEngine's module-load read threw ENOENT in the
# image and crashed the API at boot (the deploy silently rolled back for 14
# hours). The read is now hardened to optional too (belt AND suspenders),
# but the file belongs in the image.
COPY content ./content
# Persistent volume for JSONL training data (resolutions survive
# redeploys via the Coolify mount). PM2_HOME lives outside it so
# supervisor state is local to the container.
RUN mkdir -p /app/data/training /app/.pm2 \
&& chown -R vyndr:vyndr /app/data /app/.pm2 \
&& chmod +x /app/scripts/docker-entrypoint.sh
USER vyndr
EXPOSE 3001
# tini reaps zombies — important now that we spawn pm2 as a child of
# this entrypoint.
ENTRYPOINT ["/sbin/tini", "--"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -fsS http://127.0.0.1:3001/api/health || exit 1
CMD ["sh", "scripts/docker-entrypoint.sh"]