From b5983ba687b2f5ef82cdc43c84ce676d18eec35f Mon Sep 17 00:00:00 2001 From: Ricardo Mancinas Date: Wed, 22 Jul 2026 18:13:38 -0700 Subject: [PATCH] Add Swarm MySQL stack for cubex (dev/prod), deploy dev deploy/jorgecuadros-db.stack.yml: canonical internal MySQL for the platform, targeting the Portainer local endpoint on cubex (3-node Swarm). Parametrized (MYSQL_PORT / MYSQL_SERVER_ID) so one file deploys both environments as two stacks with Swarm-namespaced volumes: dev -> jorgecuadros-dev-db :3307 server-id 11 prod -> jorgecuadros-prod-db :3306 server-id 1 (replication source) Swarm-correct: named volume (no bind mount), pinned to one node via node.labels.jorgecuadros_db==true (cubex labeled), binlog+GTID enabled from the start so prod can be the VPS replication source without reconfigure. Dev deployed and verified: MySQL 8.4.10 reachable at 192.168.4.212:3307, gtid_mode ON, database jorgecuadros present. Secrets live in gitignored deploy/.env.dev, injected via Portainer stack env at deploy time. Co-Authored-By: Claude Opus 4.8 --- deploy/jorgecuadros-db.stack.yml | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 deploy/jorgecuadros-db.stack.yml diff --git a/deploy/jorgecuadros-db.stack.yml b/deploy/jorgecuadros-db.stack.yml new file mode 100644 index 0000000..bb8546e --- /dev/null +++ b/deploy/jorgecuadros-db.stack.yml @@ -0,0 +1,76 @@ +# Canonical internal MySQL for the Jorge Cuadros platform. +# +# Target: Portainer "local" endpoint on cubex (192.168.4.212:9443), which is a +# 3-node Docker Swarm. This is the source-of-truth database from the plan's +# "internal server" — the migration transform+load and the NestJS API both +# point at it, and (later) it becomes the replication SOURCE for the VPS. +# +# DEV / PROD as two stacks from this one file. Deploy it twice with different +# stack names + env_data: +# dev : stack jorgecuadros-dev-db MYSQL_PORT=3307 MYSQL_SERVER_ID=11 +# prod: stack jorgecuadros-prod-db MYSQL_PORT=3306 MYSQL_SERVER_ID=1 +# Swarm namespaces the named volume by stack name, so the two environments get +# fully isolated data (jorgecuadros-dev-db_mysql_data vs -prod-db_...) with no +# extra config. Distinct published ports let both run on the same node at once. +# server-id must be unique per environment (prod=1 is the replication source). +# +# Swarm statefulness rules (see portainer-gitea-deploy skill): +# - named volume, never a relative bind mount (the API deploy path won't +# create host dirs -> "bind source path does not exist"). +# - a named volume is LOCAL to whichever node the task lands on. With no +# shared storage, the DB MUST be pinned to one node or a reschedule would +# start against a fresh empty volume. Pinned here via a node label so it is +# not tied to a hostname: label exactly one node with +# docker node update --label-add jorgecuadros_db=true +# and MySQL will always run there, reusing the same volume. +# +# Secrets (MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD) are injected at deploy time via +# Portainer env_data / stack environment, not committed here. + +version: "3.8" + +services: + mysql: + image: mysql:8.4 + 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.) + # binlog + GTID on from day one so this node can be the replication + # SOURCE for the VPS replica later without a restart/reconfigure. + - --server-id=${MYSQL_SERVER_ID:-1} + - --log-bin=mysql-bin + - --binlog-format=ROW + - --gtid-mode=ON + - --enforce-gtid-consistency=ON + 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: + # Swarm ingress publishes on every node, so this is reachable at + # 192.168.4.212:${MYSQL_PORT} regardless of which node the task pins to. + - target: 3306 + published: ${MYSQL_PORT:-3306} + protocol: tcp + mode: ingress + volumes: + - mysql_data:/var/lib/mysql + deploy: + replicas: 1 + placement: + constraints: + - node.labels.jorgecuadros_db == true + restart_policy: + condition: any + update_config: + order: stop-first + 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: