Files
jorgecuadros-platform/deploy/jorgecuadros-db.stack.yml
rmancinasandClaude Opus 4.8 b5983ba687 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 <noreply@anthropic.com>
2026-07-22 18:13:38 -07:00

77 lines
3.2 KiB
YAML

# 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 <node>
# 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: