Files
jorgecuadros-platform/deploy/jorgecuadros-app.stack.yml
T
rmancinasandClaude Opus 5 683fd37b08 docs(deploy): stop documenting API_ORIGIN as required
The swarm stack still hard-failed on an unset API_ORIGIN, and both the env
template and the README told the reader to pin it — the exact habit the derived
origin was meant to end. Make it an optional override everywhere, and say that
WEB_ORIGIN is now a list.

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

124 lines
5.0 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
# Credentials the "Operaciones" screen runs mysqldump/mysql as. NOT the
# application user: --single-transaction needs the global RELOAD privilege
# and the app user has only ALL ON jorgecuadros.*, so every backup, sync
# and re-import fails without this. Host/port/database still come from
# DATABASE_URL — this only changes who logs in. See opsConn() in
# apps/api/src/ops/ops.service.ts.
OPS_DB_ADMIN_USER: ${OPS_DB_ADMIN_USER:-root}
OPS_DB_ADMIN_PASSWORD: ${OPS_DB_ADMIN_PASSWORD:?OPS_DB_ADMIN_PASSWORD must be set}
# 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}
# Outbound mail (SES) — runtime config, not a build-time CI secret.
SES_REGION: ${SES_REGION:-}
SES_FROM: ${SES_FROM:-}
SES_FROM_NAME: ${SES_FROM_NAME:-}
SES_ACCESS_KEY: ${SES_ACCESS_KEY:-}
SES_SECRET_KEY: ${SES_SECRET_KEY:-}
SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET:-}
NOTIFICATION_ADMIN_EMAILS: ${NOTIFICATION_ADMIN_EMAILS:-}
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:
# OPTIONAL override of the API URL the browser calls (injected at runtime,
# see layout.tsx). Leave it unset: the browser then derives the origin
# from the page it loaded — same host on port 3001 over plain HTTP, or
# /api behind a TLS-terminating proxy. Set it only when the API really
# lives on a different host than the web app.
API_ORIGIN: ${API_ORIGIN:-}
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: