Files
jorgecuadros-platform/deploy/galactus/jorgecuadros-db.compose.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

65 lines
2.9 KiB
YAML

# MySQL for the Jorge Cuadros platform on galactus — the PROD source of truth.
#
# galactus is STANDALONE Docker (Portainer endpoint 3, `swarm: inactive`), not
# the 3-node Swarm on cubex. deploy/jorgecuadros-db.stack.yml is the Swarm
# version of this file; the deltas are called out below because plain compose
# SILENTLY IGNORES the Swarm keys rather than erroring on them:
#
# 1. `deploy.restart_policy` is ignored -> `restart: unless-stopped` instead.
# Without this MySQL does not come back after a host reboot. This is the
# single highest-risk difference.
# 2. `deploy.placement.constraints` is meaningless on one host — dropped,
# along with its `docker node update --label-add jorgecuadros_db=true`
# prerequisite.
# 3. `deploy.replicas` / `update_config` are ignored — dropped.
# 4. `ports: {mode: ingress}` long syntax is Swarm-only -> short syntax.
# 5. Named volumes stay exactly as they were: the node-pinning hazard that
# motivated them was purely a Swarm problem, and Portainer still namespaces
# the volume by stack name.
#
# This node is the REPLICATION MASTER for the whole topology. Every other MySQL
# is a replica of it. server-id must be unique across the topology (prod=1,
# cubex dev=11); a duplicate silently breaks replication. binlog + GTID are on
# from first boot so a replica can attach with SOURCE_AUTO_POSITION=1 and no
# file/position bookkeeping.
#
# Keep in sync with deploy/jorgecuadros-db.stack.yml when either changes.
services:
mysql:
image: mysql:8.4
restart: unless-stopped
command:
# (caching_sha2_password is already the default in 8.4; the old
# --default-authentication-plugin flag was REMOVED in 8.4 and aborts boot.)
- --server-id=${MYSQL_SERVER_ID:-1}
- --log-bin=mysql-bin
- --binlog-format=ROW
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
# A replica offline longer than this needs a full re-seed, because the
# binlogs it still needs are gone. The 8.4 default is 30 days; raise it
# here rather than discovering the gap during an outage.
- --binlog-expire-logs-seconds=${MYSQL_BINLOG_EXPIRE_SECONDS:-5184000}
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-jorgecuadros}
MYSQL_USER: ${MYSQL_USER:-jorgecuadros}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set}
ports:
# Standalone: binds directly on the host. Reachable at
# <galactus>:${MYSQL_PORT}. Replicas connect here — see
# docs/DEPLOY_AND_MIGRATIONS.md on NOT exposing raw 3306 to the internet.
- "${MYSQL_PORT:-3306}:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 12
start_period: 40s
volumes:
mysql_data: