Compare commits

..
2 Commits
Author SHA1 Message Date
rmancinas 344319f456 chore: clarify Dockerloid workflow comments 2026-10-13 18:04:43 -07:00
rmancinas ac735a627e chore: route cubex deployment through Dockerloid 2026-10-13 17:59:42 -07:00
2 changed files with 44 additions and 45 deletions

No files matched your search

+40 -42
View File
@@ -1,7 +1,7 @@
# Manual PROD deploy to Portainer. # Manual PROD deploy to Dockerloid.
# #
# This does NOT build — build.yml already builds + pushes the api/web images. # This does NOT build — build.yml already builds + pushes the api/web images.
# This workflow (re)applies the deploy/*.stack.yml files to the Portainer Swarm. # This workflow (re)applies the deploy/*.stack.yml files to the Dockerloid Swarm.
# Trigger it by hand from the Actions tab ("Run workflow") and choose: # Trigger it by hand from the Actions tab ("Run workflow") and choose:
# - tag: which already-published image tag to ship (default: latest) # - tag: which already-published image tag to ship (default: latest)
# - scope: how much to deploy # - scope: how much to deploy
@@ -21,7 +21,7 @@
# own workflow, .gitea/workflows/deploy-galactus.yml. # own workflow, .gitea/workflows/deploy-galactus.yml.
# #
# cssnr/portainer-stack-deploy-action creates each stack on first run and updates # cssnr/portainer-stack-deploy-action creates each stack on first run and updates
# it on every run, so no manual stack pre-creation in the Portainer UI. On a # it on every run, so no manual stack pre-creation in the Dockerloid UI. On a
# `full` deploy the db + minio stacks are applied BEFORE the app (the API depends # `full` deploy the db + minio stacks are applied BEFORE the app (the API depends
# on them). db + minio are stateful + pinned to node label jorgecuadros_db=true # on them). db + minio are stateful + pinned to node label jorgecuadros_db=true
# (see their stack files) — re-applying them is idempotent and keeps their data. # (see their stack files) — re-applying them is idempotent and keeps their data.
@@ -29,13 +29,13 @@
# Prereqs (once): # Prereqs (once):
# - one swarm node labelled jorgecuadros_db=true (db + minio + api pin there). # - one swarm node labelled jorgecuadros_db=true (db + minio + api pin there).
# - Gitea repo secrets set (Settings > Actions > Secrets): # - Gitea repo secrets set (Settings > Actions > Secrets):
# # Portainer # # Dockerloid
# PORTAINER_URL https://192.168.4.212:9443 # DOCKERLOID_URL http://192.168.4.212:7777
# PORTAINER_API_KEY Portainer access token # DOCKERLOID_API_KEY Dockerloid access token
# PORTAINER_ENDPOINT_ID 2 (the local Swarm endpoint) # DOCKERLOID_ENDPOINT_ID 2 (the local Swarm endpoint)
# PORTAINER_APP_STACK_NAME e.g. jorgecuadros-prod-app # DOCKERLOID_APP_STACK_NAME e.g. jorgecuadros-prod-app
# PORTAINER_DB_STACK_NAME e.g. jorgecuadros-prod-db (full only) # DOCKERLOID_DB_STACK_NAME e.g. jorgecuadros-prod-db (full only)
# PORTAINER_MINIO_STACK_NAME e.g. jorgecuadros-prod-minio (full only) # DOCKERLOID_MINIO_STACK_NAME e.g. jorgecuadros-prod-minio (full only)
# # App runtime # # App runtime
# DATABASE_URL mysql://jorgecuadros:<pass>@192.168.4.212:3306/jorgecuadros # DATABASE_URL mysql://jorgecuadros:<pass>@192.168.4.212:3306/jorgecuadros
# SESSION_SECRET 64-hex (openssl rand -hex 32) # SESSION_SECRET 64-hex (openssl rand -hex 32)
@@ -48,7 +48,7 @@
# # Database stack (full only) # # Database stack (full only)
# MYSQL_PASSWORD app-user password (matches DATABASE_URL) # MYSQL_PASSWORD app-user password (matches DATABASE_URL)
# MYSQL_ROOT_PASSWORD mysql root password # MYSQL_ROOT_PASSWORD mysql root password
# - the runner must reach Portainer (9443). It should also reach MySQL (3306) # - runner must reach Dockerloid API (7777) and MySQL (3306)
# for the migrate step, but that is no longer load-bearing: dispatch with # for the migrate step, but that is no longer load-bearing: dispatch with
# skip_migrate=true and the api container applies the migrations itself at # skip_migrate=true and the api container applies the migrations itself at
# start (docker/api-entrypoint.sh). `migrate deploy` is idempotent, so the # start (docker/api-entrypoint.sh). `migrate deploy` is idempotent, so the
@@ -59,7 +59,7 @@
# npx prisma@5 migrate resolve --applied 0000_init \ # npx prisma@5 migrate resolve --applied 0000_init \
# --schema packages/database/prisma/schema.prisma # --schema packages/database/prisma/schema.prisma
name: Deploy to Portainer name: Deploy via Dockerloid
on: on:
workflow_dispatch: workflow_dispatch:
@@ -109,12 +109,12 @@ jobs:
# action's input, not the secret you forgot. # action's input, not the secret you forgot.
- name: Preflight — required secrets - name: Preflight — required secrets
env: env:
PORTAINER_URL: ${{ secrets.PORTAINER_URL }} PORTAINER_URL: ${{ secrets.DOCKERLOID_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} PORTAINER_API_KEY: ${{ secrets.DOCKERLOID_API_KEY }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} PORTAINER_ENDPOINT_ID: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
PORTAINER_APP_STACK_NAME: ${{ secrets.PORTAINER_APP_STACK_NAME }} DOCKERLOID_APP_STACK_NAME: ${{ secrets.DOCKERLOID_APP_STACK_NAME }}
PORTAINER_DB_STACK_NAME: ${{ secrets.PORTAINER_DB_STACK_NAME }} DOCKERLOID_DB_STACK_NAME: ${{ secrets.DOCKERLOID_DB_STACK_NAME }}
PORTAINER_MINIO_STACK_NAME: ${{ secrets.PORTAINER_MINIO_STACK_NAME }} DOCKERLOID_MINIO_STACK_NAME: ${{ secrets.DOCKERLOID_MINIO_STACK_NAME }}
DATABASE_URL: ${{ secrets.DATABASE_URL }} DATABASE_URL: ${{ secrets.DATABASE_URL }}
SESSION_SECRET: ${{ secrets.SESSION_SECRET }} SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
APP_API_ORIGIN: ${{ secrets.APP_API_ORIGIN }} APP_API_ORIGIN: ${{ secrets.APP_API_ORIGIN }}
@@ -127,11 +127,11 @@ jobs:
SCOPE: ${{ github.event.inputs.scope }} SCOPE: ${{ github.event.inputs.scope }}
run: | run: |
REQUIRED="PORTAINER_URL PORTAINER_API_KEY PORTAINER_ENDPOINT_ID REQUIRED="PORTAINER_URL PORTAINER_API_KEY PORTAINER_ENDPOINT_ID
PORTAINER_APP_STACK_NAME DATABASE_URL SESSION_SECRET DOCKERLOID_APP_STACK_NAME DATABASE_URL SESSION_SECRET
APP_API_ORIGIN APP_WEB_ORIGIN APP_S3_ENDPOINT APP_API_ORIGIN APP_WEB_ORIGIN APP_S3_ENDPOINT
MINIO_ROOT_USER MINIO_ROOT_PASSWORD MYSQL_ROOT_PASSWORD" MINIO_ROOT_USER MINIO_ROOT_PASSWORD MYSQL_ROOT_PASSWORD"
if [ "$SCOPE" = "full" ]; then if [ "$SCOPE" = "full" ]; then
REQUIRED="$REQUIRED PORTAINER_DB_STACK_NAME PORTAINER_MINIO_STACK_NAME REQUIRED="$REQUIRED DOCKERLOID_DB_STACK_NAME DOCKERLOID_MINIO_STACK_NAME
MYSQL_PASSWORD" MYSQL_PASSWORD"
fi fi
missing="" missing=""
@@ -151,12 +151,12 @@ jobs:
if: ${{ github.event.inputs.scope == 'full' }} if: ${{ github.event.inputs.scope == 'full' }}
uses: cssnr/portainer-stack-deploy-action@v1 uses: cssnr/portainer-stack-deploy-action@v1
with: with:
url: ${{ secrets.PORTAINER_URL }} url: ${{ secrets.DOCKERLOID_URL }}
token: ${{ secrets.PORTAINER_API_KEY }} token: ${{ secrets.DOCKERLOID_API_KEY }}
name: ${{ secrets.PORTAINER_DB_STACK_NAME }} name: ${{ secrets.DOCKERLOID_DB_STACK_NAME }}
file: deploy/jorgecuadros-db.stack.yml file: deploy/jorgecuadros-db.stack.yml
type: file type: file
endpoint: ${{ secrets.PORTAINER_ENDPOINT_ID }} endpoint: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
env_data: | env_data: |
{ {
"MYSQL_SERVER_ID": "1", "MYSQL_SERVER_ID": "1",
@@ -172,12 +172,12 @@ jobs:
if: ${{ github.event.inputs.scope == 'full' }} if: ${{ github.event.inputs.scope == 'full' }}
uses: cssnr/portainer-stack-deploy-action@v1 uses: cssnr/portainer-stack-deploy-action@v1
with: with:
url: ${{ secrets.PORTAINER_URL }} url: ${{ secrets.DOCKERLOID_URL }}
token: ${{ secrets.PORTAINER_API_KEY }} token: ${{ secrets.DOCKERLOID_API_KEY }}
name: ${{ secrets.PORTAINER_MINIO_STACK_NAME }} name: ${{ secrets.DOCKERLOID_MINIO_STACK_NAME }}
file: deploy/jorgecuadros-minio.stack.yml file: deploy/jorgecuadros-minio.stack.yml
type: file type: file
endpoint: ${{ secrets.PORTAINER_ENDPOINT_ID }} endpoint: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
env_data: | env_data: |
{ {
"MINIO_API_PORT": "9000", "MINIO_API_PORT": "9000",
@@ -192,9 +192,9 @@ jobs:
# unreachable by the only restore path this platform has. # unreachable by the only restore path this platform has.
- name: Pre-migrate backup - name: Pre-migrate backup
env: env:
PORTAINER_URL: ${{ secrets.PORTAINER_URL }} PORTAINER_URL: ${{ secrets.DOCKERLOID_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} PORTAINER_API_KEY: ${{ secrets.DOCKERLOID_API_KEY }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} PORTAINER_ENDPOINT_ID: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
DATABASE_URL: ${{ secrets.DATABASE_URL }} DATABASE_URL: ${{ secrets.DATABASE_URL }}
# The dump runs as root: --single-transaction issues FLUSH TABLES, # The dump runs as root: --single-transaction issues FLUSH TABLES,
# which needs the global RELOAD privilege the application user # which needs the global RELOAD privilege the application user
@@ -202,9 +202,8 @@ jobs:
MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
BACKUP_TAG: ${{ github.event.inputs.tag }} BACKUP_TAG: ${{ github.event.inputs.tag }}
ALLOW_MISSING_CONTAINER: ${{ github.event.inputs.bootstrap }} ALLOW_MISSING_CONTAINER: ${{ github.event.inputs.bootstrap }}
# Portainer serves a self-signed certificate. Scoped to this step # Dockerloid uses HTTP on the LAN; this step does not need a TLS bypass.
# only, which does nothing but talk to Portainer. # Requests in this step go only to the Dockerloid API.
NODE_TLS_REJECT_UNAUTHORIZED: "0"
run: node deploy/scripts/pre-migrate-backup.mjs run: node deploy/scripts/pre-migrate-backup.mjs
# --- schema, forward-only --------------------------------------------- # --- schema, forward-only ---------------------------------------------
@@ -234,28 +233,27 @@ jobs:
# serving an older build of the same tag. # serving an older build of the same tag.
- name: Pull images - name: Pull images
env: env:
PORTAINER_URL: ${{ secrets.PORTAINER_URL }} PORTAINER_URL: ${{ secrets.DOCKERLOID_URL }}
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }} PORTAINER_API_KEY: ${{ secrets.DOCKERLOID_API_KEY }}
PORTAINER_ENDPOINT_ID: ${{ secrets.PORTAINER_ENDPOINT_ID }} PORTAINER_ENDPOINT_ID: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
REGISTRY: ${{ env.REGISTRY }} REGISTRY: ${{ env.REGISTRY }}
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
IMAGES: ${{ github.repository_owner }}/jorgecuadros-api,${{ github.repository_owner }}/jorgecuadros-web IMAGES: ${{ github.repository_owner }}/jorgecuadros-api,${{ github.repository_owner }}/jorgecuadros-web
TAG: ${{ github.event.inputs.tag }} TAG: ${{ github.event.inputs.tag }}
NODE_TLS_REJECT_UNAUTHORIZED: "0"
run: node deploy/scripts/pull-images.mjs run: node deploy/scripts/pull-images.mjs
# --- always: the app (web + api) ------------------------------------- # --- always: the app (web + api) -------------------------------------
- name: Deploy app stack - name: Deploy app stack
uses: cssnr/portainer-stack-deploy-action@v1 uses: cssnr/portainer-stack-deploy-action@v1
with: with:
url: ${{ secrets.PORTAINER_URL }} url: ${{ secrets.DOCKERLOID_URL }}
token: ${{ secrets.PORTAINER_API_KEY }} token: ${{ secrets.DOCKERLOID_API_KEY }}
name: ${{ secrets.PORTAINER_APP_STACK_NAME }} name: ${{ secrets.DOCKERLOID_APP_STACK_NAME }}
file: deploy/jorgecuadros-app.stack.yml file: deploy/jorgecuadros-app.stack.yml
type: file type: file
pull: true pull: true
endpoint: ${{ secrets.PORTAINER_ENDPOINT_ID }} endpoint: ${{ secrets.DOCKERLOID_ENDPOINT_ID }}
# NOTE: the block below is parsed as JSON — no comments inside it. # NOTE: the block below is parsed as JSON — no comments inside it.
# #
# API_ORIGIN is deliberately absent. The browser derives the API origin # API_ORIGIN is deliberately absent. The browser derives the API origin
+4 -3
View File
@@ -1,7 +1,8 @@
# Canonical internal MySQL for the Jorge Cuadros platform. # Canonical internal MySQL for the Jorge Cuadros platform.
# #
# Target: Portainer "local" endpoint on cubex (192.168.4.212:9443), which is a # Target: cubex local endpoint, a 3-node Docker Swarm. The cubex workflow deploys
# 3-node Docker Swarm. This is the source-of-truth database from the plan's # through Dockerloid; the separate Galactus workflow uses its own Portainer. This
# is the source-of-truth database from the plan's
# "internal server" — the migration transform+load and the NestJS API both # "internal server" — the migration transform+load and the NestJS API both
# point at it, and (later) it becomes the replication SOURCE for the VPS. # point at it, and (later) it becomes the replication SOURCE for the VPS.
# #
@@ -25,7 +26,7 @@
# and MySQL will always run there, reusing the same volume. # and MySQL will always run there, reusing the same volume.
# #
# Secrets (MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD) are injected at deploy time via # Secrets (MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD) are injected at deploy time via
# Portainer env_data / stack environment, not committed here. # The stack action env_data / stack environment, not committed here.
version: "3.8" version: "3.8"