Files
jorgecuadros-platform/deploy/jorgecuadros-app.stack.yml
T
rmancinasandClaude Opus 5 4ee7ec71f0
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m49s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m2s
feat(deploy): prisma migration history, /version, galactus standalone deploy
Closes the gap between "what tag did I deploy" and "what is actually running",
and gives the schema a history that can be reasoned about across releases.

Migrations
- Baseline the existing schema as 0000_init (migrate diff --from-empty). The
  schema had only ever been applied with `prisma db push`, so no history
  existed and schema state was disconnected from app version. Existing
  databases must be baselined once with `migrate resolve --applied 0000_init`;
  the workflows print this remedy on P3005.
- Run `prisma migrate deploy` as a deploy STEP, not the container CMD — as a
  CMD, N replicas would race each other applying the same migration.

Version reporting
- GET /version on the API reports the APP_VERSION / GIT_SHA / BUILD_DATE that
  build.yml already baked into both images but nothing ever read.
- The web footer shows the web build and flags an api/web mismatch. The two
  cannot drift at build time (one matrix run) but can at deploy time.
- Both deploy workflows now fail if the running API does not report the tag
  that was dispatched — a stack naming a tag is not proof of what is running.
- scripts/set-version.mjs stamps every package.json, which had all sat at
  0.1.0 while real releases shipped as v1.x.

Pre-migrate backup
- deploy/scripts/pre-migrate-backup.mjs dumps the database from INSIDE the
  still-running old API container over Portainer's Docker API, so the file
  lands in the volume the Operaciones restore screen reads. A dump taken on
  the CI runner would be unreachable by the only restore path we have.
  Verifies the artefact with `gzip -t` before letting the migration proceed.

galactus
- deploy/galactus/*.compose.yml: standalone-Docker ports of the Swarm stacks.
  Plain compose silently ignores `deploy:`, so restart_policy becomes
  `restart: unless-stopped` — without it nothing returns after a host reboot.
- .gitea/workflows/deploy-galactus.yml drives endpoint 3 with its own secrets.

Fixes
- deploy.yml passed `endpoint_id` and `pull_image` to
  cssnr/portainer-stack-deploy-action, which has no such inputs (they are
  `endpoint` and `pull`). The endpoint was silently never set.

docs/DEPLOY_AND_MIGRATIONS.md documents expand/contract as the rule for schema
changes: Prisma has no down-migrations, so a code rollback never rolls the
schema back, and restoring the replication master from a dump diverges every
replica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:41:12 -07:00

104 lines
3.8 KiB
YAML

# Application stack for the Jorge Cuadros platform: the NestJS API + the Next.js
# web front-end. The two images are built + pushed by .gitea/workflows/build.yml:
# git.mancinas.io/rmancinas/jorgecuadros-api
# git.mancinas.io/rmancinas/jorgecuadros-web
#
# This stack does NOT ship MySQL or MinIO — those are their own stacks
# (deploy/jorgecuadros-db.stack.yml, deploy/jorgecuadros-minio.stack.yml). The
# API reaches them over the network via DATABASE_URL / S3_ENDPOINT, which point
# at the db + minio stacks' published ingress ports on the swarm host.
#
# Target: Portainer local endpoint on cubex (3-node Swarm). PROD only.
# Deploy with a stack env that supplies every ${VAR:?...} below — see
# deploy/jorgecuadros-app.env.example for the full list.
#
# Statefulness: the API keeps uploaded Access files (ingest) and DB backups on
# named volumes, which are node-local. So the API is pinned to the same node as
# the db/minio stacks (node label jorgecuadros_db == true) — a reschedule would
# otherwise start against empty ingest/backup volumes. The web tier is
# stateless and floats freely.
#
# The web image is NOT URL-baked: the browser's API origin is injected at
# runtime from API_ORIGIN (see apps/web/src/app/layout.tsx), so this same image
# works for any deployment — set the URL here, not at build time.
version: "3.8"
services:
api:
image: git.mancinas.io/rmancinas/jorgecuadros-api:${APP_TAG:-latest}
# Container label (not `deploy.labels`, which labels the swarm SERVICE).
# deploy/scripts/pre-migrate-backup.mjs finds the container by this label to
# run its pre-migrate mysqldump into the backup volume.
labels:
io.jorgecuadros.role: "api"
environment:
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
# CORS: the public origin the browser loads the web app from.
WEB_ORIGIN: ${WEB_ORIGIN:?WEB_ORIGIN must be set}
PORT: "3001"
INGEST_DIR: /data/ingest
BACKUP_DIR: /data/backups
MIGRATION_ENV: prod
# Object storage — internal endpoint the API (server-side) uses to reach
# the minio stack. Not browser-facing (downloads proxy through the API).
S3_ENDPOINT: ${S3_ENDPOINT:?S3_ENDPOINT must be set}
S3_BUCKET: ${S3_BUCKET:-jorgecuadros-documents}
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER must be set}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set}
ports:
- target: 3001
published: ${API_PORT:-3001}
protocol: tcp
mode: ingress
volumes:
- ingest_data:/data/ingest
- backup_data:/data/backups
deploy:
replicas: 1
placement:
constraints:
- node.labels.jorgecuadros_db == true
restart_policy:
condition: any
update_config:
order: stop-first
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"]
interval: 15s
timeout: 5s
retries: 10
start_period: 30s
web:
image: git.mancinas.io/rmancinas/jorgecuadros-web:${APP_TAG:-latest}
labels:
io.jorgecuadros.role: "web"
environment:
# Public API URL the browser calls (injected at runtime, see layout.tsx).
API_ORIGIN: ${API_ORIGIN:?API_ORIGIN must be set}
ports:
- target: 3000
published: ${WEB_PORT:-3000}
protocol: tcp
mode: ingress
depends_on:
- api
deploy:
replicas: 1
restart_policy:
condition: any
update_config:
order: start-first
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/ >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 5s
retries: 10
start_period: 30s
volumes:
ingest_data:
backup_data: