From 4dbd429203c1ec4b105180040a40168767a56912 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Wed, 22 Apr 2026 01:34:44 +0000 Subject: [PATCH] feat(deploy): portainer stack template for federation test instances [DEPLOY-02] (#485) --- deploy/portainer/README.md | 70 +++++++++++ deploy/portainer/federated-test.stack.yml | 147 ++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 deploy/portainer/README.md create mode 100644 deploy/portainer/federated-test.stack.yml diff --git a/deploy/portainer/README.md b/deploy/portainer/README.md new file mode 100644 index 0000000..ea93a13 --- /dev/null +++ b/deploy/portainer/README.md @@ -0,0 +1,70 @@ +# deploy/portainer/ + +Portainer stack templates for Mosaic Stack deployments. + +## Files + +| File | Purpose | +| -------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `federated-test.stack.yml` | Docker Swarm stack for federation end-to-end test instances (`mos-test-1.woltje.com`, `mos-test-2.woltje.com`) | + +--- + +## federated-test.stack.yml + +A self-contained Swarm stack that boots a federated-tier Mosaic gateway with co-located Postgres 17 (pgvector) and Valkey 8. This is a **test template** — production deployments will use a separate template with stricter resource limits and Docker secrets. + +### Deploy via Portainer UI + +1. Log into Portainer. +2. Navigate to **Stacks → Add stack**. +3. Set a stack name matching `STACK_NAME` below (e.g. `mos-test-1`). +4. Choose **Web editor** and paste the contents of `federated-test.stack.yml`. +5. Scroll to **Environment variables** and add each variable listed below. +6. Click **Deploy the stack**. + +### Required environment variables + +| Variable | Example | Notes | +| -------------------- | --------------------------------------- | -------------------------------------------------------- | +| `STACK_NAME` | `mos-test-1` | Unique per stack — used in Traefik router/service names. | +| `HOST_FQDN` | `mos-test-1.woltje.com` | Fully-qualified hostname served by this stack. | +| `POSTGRES_PASSWORD` | _(generate randomly)_ | Database password. Do **not** reuse between stacks. | +| `BETTER_AUTH_SECRET` | _(generate: `openssl rand -base64 32`)_ | BetterAuth session signing key. | +| `BETTER_AUTH_URL` | `https://mos-test-1.woltje.com` | Public base URL of the gateway. | + +Optional variables (uncomment in the YAML or set in Portainer): + +| Variable | Notes | +| ----------------------------- | ---------------------------------------------------------- | +| `ANTHROPIC_API_KEY` | Enable Claude models. | +| `OPENAI_API_KEY` | Enable OpenAI models. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Forward traces to a collector (e.g. `http://jaeger:4318`). | + +### Required external resources + +Before deploying, ensure the following exist on the Swarm: + +1. **`traefik-public` overlay network** — shared network Traefik uses to route traffic to stacks. + ```bash + docker network create --driver overlay --attachable traefik-public + ``` +2. **`letsencrypt` cert resolver** — configured in the Traefik Swarm stack. The stack template references `tls.certresolver=letsencrypt`; the name must match your Traefik config. +3. **DNS A record** — `${HOST_FQDN}` must resolve to the Swarm ingress IP (or a Cloudflare-proxied address pointing there). + +### Deployed instances + +| Stack name | HOST_FQDN | Purpose | +| ------------ | ----------------------- | ---------------------------------- | +| `mos-test-1` | `mos-test-1.woltje.com` | DEPLOY-03 — first federation peer | +| `mos-test-2` | `mos-test-2.woltje.com` | DEPLOY-04 — second federation peer | + +### Image + +The gateway image is pinned by digest to `fed-v0.1.0-m1` (verified in DEPLOY-01). Update the digest in the YAML when promoting a new build — never use `:latest` or a mutable tag in Swarm. + +### Notes + +- This template boots a **vanilla M1-baseline gateway** in federated tier. Federation grants (Step-CA, mTLS) are M2+ scope and not included here. +- Each stack gets its own Postgres volume (`postgres-data`) and Valkey volume (`valkey-data`) scoped to the stack name by Swarm. +- `depends_on` is honoured by Compose but ignored by Swarm — healthchecks on Postgres and Valkey ensure the gateway retries until they are ready. diff --git a/deploy/portainer/federated-test.stack.yml b/deploy/portainer/federated-test.stack.yml new file mode 100644 index 0000000..089526b --- /dev/null +++ b/deploy/portainer/federated-test.stack.yml @@ -0,0 +1,147 @@ +# deploy/portainer/federated-test.stack.yml +# +# Portainer / Docker Swarm stack template — federated-tier test instance +# +# PURPOSE +# Deploys a single federated-tier Mosaic gateway with co-located Postgres +# (pgvector) and Valkey for end-to-end federation testing. Intended for +# mos-test-1.woltje.com and mos-test-2.woltje.com (DEPLOY-03/04). +# +# REQUIRED ENV VARS (set per-stack in Portainer → Stacks → Environment variables) +# STACK_NAME Unique name for Traefik router/service labels. +# Examples: mos-test-1, mos-test-2 +# HOST_FQDN Fully-qualified domain name served by this stack. +# Examples: mos-test-1.woltje.com, mos-test-2.woltje.com +# POSTGRES_PASSWORD Database password — set per stack; do NOT commit a default. +# BETTER_AUTH_SECRET Random 32-char string for BetterAuth session signing. +# Generate: openssl rand -base64 32 +# BETTER_AUTH_URL Public gateway base URL, e.g. https://mos-test-1.woltje.com +# +# OPTIONAL ENV VARS (uncomment and set in Portainer to enable features) +# ANTHROPIC_API_KEY sk-ant-... +# OPENAI_API_KEY sk-... +# OTEL_EXPORTER_OTLP_ENDPOINT http://:4318 +# OTEL_SERVICE_NAME (default: mosaic-gateway) +# +# REQUIRED EXTERNAL RESOURCES +# traefik-public Docker overlay network — must exist before deploying. +# Create: docker network create --driver overlay --attachable traefik-public +# letsencrypt Traefik cert resolver configured on the Swarm manager. +# DNS A record ${HOST_FQDN} → Swarm ingress IP (or Cloudflare proxy). +# +# IMAGE +# Pinned to digest fed-v0.1.0-m1 (DEPLOY-01 verified). +# Update digest here when promoting a new build. +# +# NOTE: This is a TEST template — production deployments use a separate +# parameterised template with stricter resource limits and secrets. + +version: '3.9' + +services: + gateway: + image: git.mosaicstack.dev/mosaicstack/stack/gateway@sha256:9b72e202a9eecc27d31920b87b475b9e96e483c0323acc57856be4b1355db1ec + # Tag for human reference: fed-v0.1.0-m1 + environment: + # ── Tier ─────────────────────────────────────────────────────────────── + MOSAIC_TIER: federated + + # ── Database ─────────────────────────────────────────────────────────── + DATABASE_URL: postgres://gateway:${POSTGRES_PASSWORD}@postgres:5432/mosaic + + # ── Queue ────────────────────────────────────────────────────────────── + VALKEY_URL: redis://valkey:6379 + + # ── Gateway ──────────────────────────────────────────────────────────── + GATEWAY_PORT: '3000' + GATEWAY_CORS_ORIGIN: https://${HOST_FQDN} + + # ── Auth ─────────────────────────────────────────────────────────────── + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + BETTER_AUTH_URL: https://${HOST_FQDN} + + # ── Observability ────────────────────────────────────────────────────── + OTEL_SERVICE_NAME: ${STACK_NAME:-mosaic-gateway} + # OTEL_EXPORTER_OTLP_ENDPOINT: http://:4318 + + # ── AI Providers (uncomment to enable) ───────────────────────────────── + # ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + # OPENAI_API_KEY: ${OPENAI_API_KEY} + networks: + - federated-test + - traefik-public + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + labels: + - 'traefik.enable=true' + - 'traefik.docker.network=traefik-public' + - 'traefik.http.routers.${STACK_NAME}.rule=Host(`${HOST_FQDN}`)' + - 'traefik.http.routers.${STACK_NAME}.entrypoints=websecure' + - 'traefik.http.routers.${STACK_NAME}.tls=true' + - 'traefik.http.routers.${STACK_NAME}.tls.certresolver=letsencrypt' + - 'traefik.http.services.${STACK_NAME}.loadbalancer.server.port=3000' + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/health'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + depends_on: + - postgres + - valkey + + postgres: + image: pgvector/pgvector:pg17 + environment: + POSTGRES_USER: gateway + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: mosaic + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - federated-test + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U gateway'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + valkey: + image: valkey/valkey:8-alpine + volumes: + - valkey-data:/data + networks: + - federated-test + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + healthcheck: + test: ['CMD', 'valkey-cli', 'ping'] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + +volumes: + postgres-data: + valkey-data: + +networks: + federated-test: + driver: overlay + traefik-public: + external: true