Add the missing api/web deployment path on top of the existing image build CI. - deploy/jorgecuadros-app.stack.yml: PROD app stack (api + web) pulling the git.mancinas.io registry images. Does not ship mysql/minio (separate stacks); API reaches them via DATABASE_URL / S3_ENDPOINT. API pinned to the jorgecuadros_db node for stable ingest/backup volumes; web is stateless. - deploy/jorgecuadros-app.env.example: documented stack env template. - .gitea/workflows/deploy.yml: manual (workflow_dispatch) deploy to Portainer via cssnr/portainer-stack-deploy-action. Inputs: image tag + scope (app = web+api, full = db+minio+app, applied db->minio->app). Make the web API origin runtime-configurable instead of build-baked: the root layout injects window.__API_ORIGIN__ from the API_ORIGIN env (force-dynamic) and lib/api.ts resolves it at runtime, so one built image serves any deployment. Also: dev.sh to run both dev servers (frees stale ports first) and move local dev to ports web 4500 / api 4501. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
5.5 KiB
YAML
135 lines
5.5 KiB
YAML
# Manual PROD deploy to Portainer.
|
|
#
|
|
# 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.
|
|
# Trigger it by hand from the Actions tab ("Run workflow") and choose:
|
|
# - tag: which already-published image tag to ship (default: latest)
|
|
# - scope: how much to deploy
|
|
# app = web + api only (the usual app release) [default]
|
|
# full = db + minio + web + api (bring up / update the whole platform)
|
|
#
|
|
# 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
|
|
# `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
|
|
# (see their stack files) — re-applying them is idempotent and keeps their data.
|
|
#
|
|
# Prereqs (once):
|
|
# - one swarm node labelled jorgecuadros_db=true (db + minio + api pin there).
|
|
# - Gitea repo secrets set (Settings > Actions > Secrets):
|
|
# # Portainer
|
|
# PORTAINER_URL https://192.168.4.212:9443
|
|
# PORTAINER_API_KEY Portainer access token
|
|
# PORTAINER_ENDPOINT_ID 2 (the local Swarm endpoint)
|
|
# PORTAINER_APP_STACK_NAME e.g. jorgecuadros-prod-app
|
|
# PORTAINER_DB_STACK_NAME e.g. jorgecuadros-prod-db (full only)
|
|
# PORTAINER_MINIO_STACK_NAME e.g. jorgecuadros-prod-minio (full only)
|
|
# # App runtime
|
|
# DATABASE_URL mysql://jorgecuadros:<pass>@192.168.4.212:3306/jorgecuadros
|
|
# SESSION_SECRET 64-hex (openssl rand -hex 32)
|
|
# APP_API_ORIGIN http://192.168.4.212:3001 (browser-facing API URL)
|
|
# APP_WEB_ORIGIN http://192.168.4.212:3000 (web public origin, API CORS)
|
|
# APP_S3_ENDPOINT http://192.168.4.212:9000 (server-side minio URL)
|
|
# # Object storage (app + minio stack)
|
|
# MINIO_ROOT_USER minio access key
|
|
# MINIO_ROOT_PASSWORD minio secret key
|
|
# # Database stack (full only)
|
|
# MYSQL_PASSWORD app-user password (matches DATABASE_URL)
|
|
# MYSQL_ROOT_PASSWORD mysql root password
|
|
|
|
name: Deploy to Portainer
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Image tag to deploy (latest, sha-<short>, or vX.Y.Z)"
|
|
required: true
|
|
default: "latest"
|
|
scope:
|
|
description: "What to deploy"
|
|
type: choice
|
|
required: true
|
|
default: "app"
|
|
options:
|
|
- app
|
|
- full
|
|
|
|
env:
|
|
REGISTRY: git.mancinas.io
|
|
|
|
jobs:
|
|
deploy:
|
|
name: Deploy (${{ github.event.inputs.scope }})
|
|
runs-on: docker
|
|
container:
|
|
image: node:18-alpine
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# --- full only: database ---------------------------------------------
|
|
- name: Deploy database stack
|
|
if: ${{ github.event.inputs.scope == 'full' }}
|
|
uses: cssnr/portainer-stack-deploy-action@v1
|
|
with:
|
|
url: ${{ secrets.PORTAINER_URL }}
|
|
token: ${{ secrets.PORTAINER_API_KEY }}
|
|
name: ${{ secrets.PORTAINER_DB_STACK_NAME }}
|
|
file: deploy/jorgecuadros-db.stack.yml
|
|
type: file
|
|
endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }}
|
|
env_data: |
|
|
{
|
|
"MYSQL_SERVER_ID": "1",
|
|
"MYSQL_PORT": "3306",
|
|
"MYSQL_DATABASE": "jorgecuadros",
|
|
"MYSQL_USER": "jorgecuadros",
|
|
"MYSQL_PASSWORD": "${{ secrets.MYSQL_PASSWORD }}",
|
|
"MYSQL_ROOT_PASSWORD": "${{ secrets.MYSQL_ROOT_PASSWORD }}"
|
|
}
|
|
|
|
# --- full only: object storage ---------------------------------------
|
|
- name: Deploy minio stack
|
|
if: ${{ github.event.inputs.scope == 'full' }}
|
|
uses: cssnr/portainer-stack-deploy-action@v1
|
|
with:
|
|
url: ${{ secrets.PORTAINER_URL }}
|
|
token: ${{ secrets.PORTAINER_API_KEY }}
|
|
name: ${{ secrets.PORTAINER_MINIO_STACK_NAME }}
|
|
file: deploy/jorgecuadros-minio.stack.yml
|
|
type: file
|
|
endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }}
|
|
env_data: |
|
|
{
|
|
"MINIO_API_PORT": "9000",
|
|
"MINIO_CONSOLE_PORT": "9001",
|
|
"MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}",
|
|
"MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}"
|
|
}
|
|
|
|
# --- always: the app (web + api) -------------------------------------
|
|
- name: Deploy app stack
|
|
uses: cssnr/portainer-stack-deploy-action@v1
|
|
with:
|
|
url: ${{ secrets.PORTAINER_URL }}
|
|
token: ${{ secrets.PORTAINER_API_KEY }}
|
|
name: ${{ secrets.PORTAINER_APP_STACK_NAME }}
|
|
file: deploy/jorgecuadros-app.stack.yml
|
|
type: file
|
|
pull_image: true
|
|
endpoint_id: ${{ secrets.PORTAINER_ENDPOINT_ID }}
|
|
env_data: |
|
|
{
|
|
"APP_TAG": "${{ github.event.inputs.tag }}",
|
|
"API_PORT": "3001",
|
|
"WEB_PORT": "3000",
|
|
"S3_BUCKET": "jorgecuadros-documents",
|
|
"API_ORIGIN": "${{ secrets.APP_API_ORIGIN }}",
|
|
"WEB_ORIGIN": "${{ secrets.APP_WEB_ORIGIN }}",
|
|
"S3_ENDPOINT": "${{ secrets.APP_S3_ENDPOINT }}",
|
|
"DATABASE_URL": "${{ secrets.DATABASE_URL }}",
|
|
"SESSION_SECRET": "${{ secrets.SESSION_SECRET }}",
|
|
"MINIO_ROOT_USER": "${{ secrets.MINIO_ROOT_USER }}",
|
|
"MINIO_ROOT_PASSWORD": "${{ secrets.MINIO_ROOT_PASSWORD }}"
|
|
}
|