# Deployment Guide This document covers production deployment for the `omo-bot` Coolify project with **two separate resources**: - `omo-bot-backend` (Node.js bot + Admin API) - `omo-bot-dashboard` (React/Vite static dashboard from `admin-dashboard/`) ## Deployment Topology Recommended domains: - `api.openmicodyssey.com` -> `omo-bot-backend` - `admin.openmicodyssey.com` -> `omo-bot-dashboard` Recommended edge topology: - Internet -> Nginx reverse proxy -> Coolify resources - Nginx routes by hostname to backend/dashboard resource domains or internal ports Optional same-origin pattern: - `admin.openmicodyssey.com` serves dashboard and reverse-proxies `/admin/*` + `/health` to backend. ## 1. Create Coolify Project and Resources 1. In Coolify, create (or open) project `omo-bot`. 2. Add resource `omo-bot-backend` from this repository: - Build context: repository root - Start command: `npm start` - Internal port: `8787` (Admin API) 3. Add resource `omo-bot-dashboard` from this repository: - Base directory: `admin-dashboard` - Build command: `npm run build` - Publish directory: `/` - Internal port: `80` (typical static web port behind Coolify) 4. Configure domains for each resource and enable TLS certificates. ## 1.1 Nginx Reverse Proxy in Front of Coolify Use this pattern when you want one public edge server in front of Coolify-managed services. Recommended flow: 1. Public DNS for `api.openmicodyssey.com` and `admin.openmicodyssey.com` points to Nginx host. 2. Nginx terminates TLS and forwards traffic to Coolify resource domains or private IP:port targets. 3. Coolify resources stay private or restricted to internal network where possible. Example host-based routing: ```nginx server { listen 80; server_name api.openmicodyssey.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name api.openmicodyssey.com; ssl_certificate /etc/letsencrypt/live/api.openmicodyssey.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.openmicodyssey.com/privkey.pem; location / { proxy_pass http://coolify-backend.internal:8787; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } } server { listen 80; server_name admin.openmicodyssey.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name admin.openmicodyssey.com; ssl_certificate /etc/letsencrypt/live/admin.openmicodyssey.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/admin.openmicodyssey.com/privkey.pem; location / { proxy_pass http://coolify-dashboard.internal:80; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } } ``` If you prefer same-origin routing for dashboard + API through one public domain, proxy `location /admin/` and `location /health` to backend while keeping `/` on dashboard. Recommended OAuth callback route: - `https://admin.openmicodyssey.com/oauth/discord/callback` If you use this callback path, make sure dashboard hosting serves the SPA entrypoint for that route instead of returning `404`. ## 2. Configure Runtime Variables Backend (`omo-bot-backend`) minimum: - `DATABASE_URL` - `CONFIG_DB_ENABLED=true` - Any temporary legacy vars needed for first-time `db:seed` Dashboard (`omo-bot-dashboard`) minimum: - `VITE_ADMIN_API_BASE_URL` (for split-domain deployment, set to `https://api.openmicodyssey.com`) - `VITE_DISCORD_CLIENT_ID` - `VITE_DISCORD_REDIRECT_URI` (recommended: `https://admin.openmicodyssey.com/oauth/discord/callback`) OAuth alignment: - Discord Developer Portal redirect URI must exactly match the dashboard callback URL. - Config DB key `OAUTH_BRIDGE_REDIRECT_URI` must match the same callback URL. - Dashboard env `VITE_DISCORD_REDIRECT_URI` must match the same callback URL. Discord OAuth authorize URL shape: ```text https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https%3A%2F%2Fadmin.openmicodyssey.com%2Foauth%2Fdiscord%2Fcallback&scope=identify%20guilds&prompt=consent ``` ## 3. Get Coolify API Token and Resource UUIDs 1. In Coolify, open `Keys & Tokens` -> `API Tokens` and create a token. 2. Copy your Coolify base URL (example: `https://coolify.yourdomain.com`). 3. Find resource UUID for: - `omo-bot-backend` - `omo-bot-dashboard` Deploy API endpoint used by CI: - `GET /api/v1/deploy?uuid=` - Authorization: `Bearer ` ### 3.1 Token Requirements (Important) If CI fails with `Coolify API token rejected during preflight (HTTP 403)`, token exists but lacks access to API target. Use this checklist: 1. Create token in same Coolify instance that hosts `omo-bot-backend` and `omo-bot-dashboard` resources. 2. Ensure token owner has access to project/team containing those resources. 3. Ensure token is API token (not UI session token/cookie). 4. Set `COOLIFY_BASE_URL` to Coolify root URL only (example: `https://coolify.yourdomain.com`), no extra path like `/project/...`. 5. Rotate token if unsure, then update Gitea secret `COOLIFY_API_TOKEN`. Required permissions to grant (minimum): - Project/resource read access for both deploy targets (`omo-bot-backend`, `omo-bot-dashboard`) - Deploy/trigger permission on both resources - Team/project membership that includes those resources (not observer/read-only) If your Coolify token form shows these permissions: - root - write - deploy - read - read:sensitive Use this selection for CI deploy token: - Required: read + deploy - Optional: write (only if your instance still blocks deploy with read+deploy) - Avoid for CI: root - Optional and sensitive: read:sensitive (only needed when debugging APIs that must return secrets/log internals) Tip: Start with read + deploy. If preflight still returns 403, temporarily add write to verify scope mismatch, then reduce to least-privilege that still works. Preflight check from local shell (should NOT return `401` or `403`): ```bash curl -i \ -H "Authorization: Bearer $COOLIFY_API_TOKEN" \ "$COOLIFY_BASE_URL/api/v1/deploy" ``` Resource trigger checks (match CI fallback logic): ```bash # Variant 1: GET + Bearer curl -i -G \ -H "Authorization: Bearer $COOLIFY_API_TOKEN" \ --data-urlencode "uuid=$COOLIFY_RESOURCE_UUID" \ "$COOLIFY_BASE_URL/api/v1/deploy" # Variant 2: POST JSON + Bearer curl -i -X POST \ -H "Authorization: Bearer $COOLIFY_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"uuid\":\"$COOLIFY_RESOURCE_UUID\"}" \ "$COOLIFY_BASE_URL/api/v1/deploy" # Variant 3: GET + raw Authorization token curl -i -G \ -H "Authorization: $COOLIFY_API_TOKEN" \ --data-urlencode "uuid=$COOLIFY_RESOURCE_UUID" \ "$COOLIFY_BASE_URL/api/v1/deploy" ``` If all variants return `403`, issue is permissions/scope in Coolify, not Gitea secret injection. ## 4. Configure Gitea Action Secrets/Variables In Gitea repository settings, add Actions secrets: - `COOLIFY_BASE_URL` - `COOLIFY_API_TOKEN` - `COOLIFY_RESOURCE_UUID_BOT` - `COOLIFY_RESOURCE_UUID_DASHBOARD` Current workflow in `.gitea/workflows/ci-cd.yml` triggers backend and dashboard deploys separately via the Coolify Deploy API. ## 5. Domain and DNS Checklist 1. Create DNS records to Nginx edge host (or directly to Coolify if no edge proxy): - `A` record for root/subdomain targets - `CNAME` where appropriate 2. Verify certificates issued on Nginx or Coolify, depending on TLS termination point. 3. Verify Nginx upstreams match deployed ports: - backend -> `8787` - dashboard -> `80` 4. Verify OAuth callback route resolves through dashboard hosting: - `https://admin.openmicodyssey.com/oauth/discord/callback` 5. Verify dashboard can call API at configured `VITE_ADMIN_API_BASE_URL`. 6. Verify API health endpoint over TLS: - `GET https://api.openmicodyssey.com/health` ## 6. Post-Deploy Verification 1. `npm run register:commands` (if bot app/guild IDs changed). 2. Confirm bot online in Discord and slash commands visible. 3. Open dashboard URL and run OAuth login flow. 4. Confirm Discord redirects back to `https://admin.openmicodyssey.com/oauth/discord/callback` without `404`. 5. Validate Admin API auth behavior with and without bearer token. ## 7. Coolify 403 Troubleshooting When CI fails in `Validate Coolify API access` with HTTP `403`: 1. Confirm token belongs to correct Coolify instance and team/project. 2. Confirm resource UUIDs are from same instance and still valid. 3. Re-run local preflight curl from section 3.1. 4. Recreate token and update Gitea secret. 5. Re-run pipeline. Current CI workflow already prints sanitized response snippets for deploy failures and tries auth/method fallback variants in `.gitea/workflows/ci-cd.yml`.