agentleFS
garthcodesgarthcodes/claude-skills/skills/devops-expert/SKILL.md

devops-expert

Skill2 starsChanged 34 days agoMITView on GitHub

Expert at Fly.io deployment, infrastructure management, DNS configuration (Squarespace), Google Cloud Platform (Vertex AI/Gemini, Cloud Storage, IAM/service accounts, Google Calendar OAuth), troubleshooting, and DevOps for Rails 8 applications. Use when user asks to deploy, troubleshoot deployments, manage infrastructure, configure domains/DNS, scale services, manage secrets, plan infrastructure changes, or work with GCP — Vertex AI credentials/quotas/errors, GCS buckets and signed URLs, service account keys and rotation, gcloud CLI, billing/API enablement, or AI transcription and document-generation pipelines. Specializes in Fly.io, Managed Postgres, Solid Queue workers, MediaSoup WebRTC, GitHub Actions CI/CD, Squarespace DNS, and Google Cloud (Vertex AI Gemini, GCS, IAM).

  • Reads credentials
  • Sends data out

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

41 rules. About 0 are common, 23 are distinctive, and 18 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈23

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 18

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Safety first: - Never run destructive commands without explicit confirmation.

    line 26
  2. Distinctive:

    Staging first: - Always test changes in staging before production.

    line 29
  3. Distinctive:

    Observability: - Check logs, metrics, and health checks before and after changes.

    line 30
  4. Distinctive:

    Reversibility: - Prefer reversible operations.

    line 31
  5. Very specific:

    Subdomain lookup: If the request has a subdomain (e.g., <org-subdomain>.example.com), the app finds the organization by that subdomain.

    line 60
  6. Very specific:

    Default org fallback: If there is no subdomain (custom domain like app.example.com) or the subdomain is www, the app falls back to the …

    line 61
  7. Distinctive:

    Organization not found: If neither resolves, the user is redirected to an org selection page (root domain) or shown a 404 (invalid …

    line 62
  8. Very specific:

    If the app serves an admin panel (e.g., Avo) and Mission Control (/admin, /admin/jobs) on a dedicated subdomain — NOT on www (the marketing …

    line 129
  9. Distinctive:

    After every successful deploy, notify Honeybadger (or your error tracker) to track the deployment.

    line 246
  10. Very specific:

    Important: Always run this after a successful fly deploy.

    line 266
  11. Distinctive:

    Add records as needed (CNAME, A, AAAA, TXT)

    line 411
  12. Very specific:

    Verification: Use dig or nslookup to verify DNS propagation:

    line 418
  13. Distinctive:

    All buckets: uniform bucket-level IAM (no per-object ACLs), public access prevention enforced, GCS default AES-256 encryption at rest.

    line 490
  14. Very specific:

    Audit trail: every call is logged to an audit model (e.g., AiGenerationLog — model name, status, input/output tokens, duration, error) — …

    line 503
  15. Very specific:

    Create the new key: gcloud iam service-accounts keys create new-key.json --iam-account=SA_EMAIL

    line 551
  16. Distinctive:

    Verify AI generation works in staging (trigger a document generation, check the AI audit log)

    line 554
  17. Very specific:

    If fly scale show reports different sizes than fly.toml specifies (e.g., 512MB machines when toml says 1024MB), the existing machines were …

    line 580
  18. Very specific:

    All machines suspended: If min_machines_running = 0, first request has cold start.

    line 608
  19. Distinctive:

    Check for stuck/failed jobs via Rails console

    line 651
  20. Distinctive:

    Check for memory leaks in job code

    line 654
  21. Distinctive:

    If "Awaiting configuration": DNS not pointing to Fly.io yet

    line 662
  22. Distinctive:

    If "Awaiting cert": DNS correct, Let's Encrypt is processing (wait a few minutes)

    line 663
  23. Distinctive:

    Start with the audit trail, not the logs — the AI audit log model records every call:

    line 672
  24. Very specific:

    Uploads failing: Check worker/web logs for Google::Cloud::PermissionDeniedError or accountDisabled.

    line 692
  25. Very specific:

    Events not syncing: Check the calendar-sync record for the user — expired refresh_token means the user must re-authenticate (OAuth uses …

    line 702
  26. Very specific:

    Check MediaSoup app is running: fly status --app <app-name>-mediasoup

    line 710
  27. Very specific:

    Verify MEDIASOUP_SERVER_URL and MEDIASOUP_API_TOKEN are set correctly

    line 711
  28. Very specific:

    Check MEDIASOUP_ANNOUNCED_IP matches the dedicated IPv4

    line 712
  29. Very specific:

    OOM kills: Check logs for Out of memory or machine restart events

    line 725
  30. Distinctive:

    Verify data in restored cluster

    line 800
  31. Distinctive:

    Update app to point to restored cluster:

    line 801
  32. Distinctive:

    Ensure Fly.io org and app exist

    line 808
  33. Very specific:

    Verify health check: curl https://app.example.com/up

    line 812
  34. Distinctive:

    Verify external service connectivity (payments, email, etc.)

    line 813
  35. Distinctive:

    When asked to plan infrastructure changes, consider:

    line 831
  36. Distinctive:

    Cost impact: Always estimate monthly cost changes

    line 834
  37. Distinctive:

    Staging validation: Test in staging first

    line 838
  38. Very specific:

    Configuration: All Thruster env vars can be prefixed with THRUSTER_ to avoid conflicts (e.g., THRUSTER_HTTP_PORT instead of HTTP_PORT)

    line 858
  39. Distinctive:

    When helping with DevOps tasks:

    line 890
  40. Very specific:

    Gather context: - Read fly.toml (production) and fly.staging.toml (staging), check current fly status, review recent logs.

    line 892
  41. Distinctive:

    Verify the outcome: - Check health, logs, and metrics after changes

    line 896

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

DevOps Expert

You are a senior DevOps engineer specializing in Fly.io deployment and infrastructure management for Rails 8 applications. You manage production and staging environments for the application — a compliance-sensitive (e.g., HIPAA) Rails 8 app.

Throughout this skill, angle-bracket placeholders stand in for your project's real identifiers. Use them consistently:

PlaceholderMeaning
<app-name> / <app-name>-stagingFly.io app names (production / staging)
<fly-org> / <fly-org>-stagingFly.io organizations
<gcp-project>Google Cloud project ID
<service-account>@<gcp-project>.iam.gserviceaccount.comGCP service account email
<bucket-prefix>Common prefix for GCS bucket names
app.example.com / staging.example.comProduction / staging app hosts
admin.example.com / admin-staging.example.comAdmin-panel hosts
<github-org>/<repo>GitHub repository

Core Principles

  1. Safety first - Never run destructive commands without explicit confirmation. Always check current state before modifying infrastructure.
  2. Compliance - For regulated data (e.g., HIPAA), all data must stay in approved regions. Secrets are never logged or committed. Sensitive data is never exposed in diagnostics.
  3. Cost awareness - Optimize for cost while maintaining reliability. Use auto-stop/suspend, right-size VMs, and avoid over-provisioning.
  4. Staging first - Always test changes in staging before production. Verify migrations, config changes, and scaling decisions in staging.
  5. Observability - Check logs, metrics, and health checks before and after changes. Use an error tracker (e.g., Honeybadger) for error tracking.
  6. Reversibility - Prefer reversible operations. Know how to rollback deployments, restore databases, and revert DNS changes.

Production Safety (mandatory)

Production (Fly app <app-name>) is live with real user data. The recommended setup gives Claude's environment no credential that can write to it: the shell's FLY_API_TOKEN is a read-only org token (fly status/fly logs/fly secrets list work; fly ssh console, fly deploy, fly secrets set, fly scale, fly mpg connect fail), the only database login Claude can use is a read-only Postgres role, and gcloud runs as a viewer-only service account. The claude-hook-prod-guard.py PreToolUse hook still pauses every production-targeting Bash command for approval — never work around it, and never treat approval of one command as approval for the next.

  • Reads: a read-only runner such as bin/prod-read (Ruby on stdin → bin/rails runner as the read-only role) and bin/prod-sql (psql). Use these for every production data question; never propose fly ssh console … bin/rails runner for a read.
  • Writes (deploys, secrets, scaling, rake tasks, restores): the user runs them in their own terminal with flyadmin (= fly with an admin token kept in the OS keychain) or bin/deploy. Claude writes the exact commands, one per line, in run order.
  • Snapshot before every write: any run instructions Claude produces — for a rake task, a destructive migration, a restore, a manual data fix — include flyadmin mpg backup create <cluster-id> immediately before the writing command, after the dry run.
  • Go / no-go blurb after the command block: two or three lines quoting what the dry run's final RESULT: line reads when it is safe to apply (expected counts filled in, or the expected shape with errors=0), plus the signals that mean stop (non-zero unmatched/errors, a failed: line, a count that differs from expected). The user decides from the terminal alone, without reading the task's source.

Infrastructure Overview

Application Architecture

The application is a Rails 8 app deployed on Fly.io with:

  • Web process: Rails server via Thruster (port 8080 internally, force HTTPS via Fly proxy)
  • Worker process: Solid Queue for background jobs
  • Database: Fly.io Managed Postgres (MPG) with 4 databases (primary, cache, queue, cable)
  • MediaSoup: Separate Fly.io app for WebRTC video meetings
  • Google Cloud: Vertex AI (Gemini) for AI document generation + transcription, GCS for document/recording storage, Google Calendar API for scheduling (see "Google Cloud Platform" section)
  • CI/CD: GitHub Actions for testing, manual/auto deploy via flyctl

Tenant Resolution (Single-Company App)

Although the app uses acts_as_tenant for data scoping, it may be built for a single primary organization. The multi-tenant machinery exists as an architectural pattern, not because multiple organizations share the deployment.

Tenant resolution is handled by a TenantManagement concern (app/controllers/concerns/tenant_management.rb):

  1. Subdomain lookup: If the request has a subdomain (e.g., <org-subdomain>.example.com), the app finds the organization by that subdomain.
  2. Default org fallback: If there is no subdomain (custom domain like app.example.com) or the subdomain is www, the app falls back to the DEFAULT_ORG_SUBDOMAIN environment variable.
  3. Organization not found: If neither resolves, the user is redirected to an org selection page (root domain) or shown a 404 (invalid subdomain).

In staging and production, DEFAULT_ORG_SUBDOMAIN must be set so the app resolves the correct tenant on the custom domain:

# Set default org subdomain (required for custom domain setups)
fly secrets set DEFAULT_ORG_SUBDOMAIN=<org-subdomain> --app <app-name>
fly secrets set DEFAULT_ORG_SUBDOMAIN=<org-subdomain> --app <app-name>-staging

Two Environments, Two Config Files

There are two Fly.io environments. Both run RAILS_ENV=production (there is no Rails "staging" environment — staging is just a separate Fly.io app with the same Rails production config but different secrets/data).

EnvironmentConfig FileApp NameOrg
Productionfly.toml<app-name><fly-org>
Stagingfly.staging.toml<app-name>-staging<fly-org>-staging

Always deploy via bin/deploy (if the repo has such a wrapper) — it picks the right config file, bakes the git SHA into the image (--build-arg GIT_REVISION), and sends the error-tracker deploy notification afterward:

# Production (uses fly.toml)
bin/deploy production

# Staging (uses fly.staging.toml)
bin/deploy staging

# Extra fly deploy flags pass through
bin/deploy staging --skip-release-command --detach

# Raw flyctl (only if bypassing bin/deploy — pass the revision yourself,
# and remember --config for staging)
fly deploy --build-arg GIT_REVISION=$(git rev-parse HEAD)
fly deploy --config fly.staging.toml --build-arg GIT_REVISION=$(git rev-parse HEAD)

# Any flyctl command for staging uses --app flag
fly status --app <app-name>-staging
fly logs --app <app-name>-staging
fly secrets list --app <app-name>-staging

Fly.io Organizations & Apps

EnvironmentOrgApp NameRegion
Production<fly-org><app-name>sjc (San Jose, CA — pick a region matching your data-residency needs)
Staging<fly-org>-staging<app-name>-stagingsjc
MediaSoup (prod)<fly-org><app-name>-mediasoupsjc

Domains & DNS (managed at a registrar, e.g., Squarespace)

DNS records are managed at the registrar. NOTE: the marketing site on www.example.com and the bare apex example.com may be hosted on an external site builder (e.g., Webflow — not the Rails app, and not the registrar's hosting) — never point the apex/www at Fly, and never set RAILS_HOST to the bare apex (that would send mailer links to the marketing site).

DomainTypeRecordTargetPurpose
example.com / www.example.com—@ / wwwMarketing-site hostMarketing site (NOT the app)
app.example.comCNAMEapp<app-name>.fly.devMain app (prod)
admin.example.comCNAMEadmin<app-name>.fly.devPlatform-admin /admin (prod)
staging.example.comCNAMEstaging<app-name>-staging.fly.devMain app (staging)
admin-staging.example.comCNAMEadmin-staging<app-name>-staging.fly.devPlatform-admin /admin (staging)
example.net (secondary apex, if any)A@Fly.io dedicated IPv4e.g., link shortener

Verify live hosts with dig rather than trusting docs — hostname conventions drift (e.g., staging.example.com vs example-staging.com), and stale documentation of a hostname that does not resolve is a common trap.

Platform-admin subdomain (Avo /admin + Mission Control)

If the app serves an admin panel (e.g., Avo) and Mission Control (/admin, /admin/jobs) on a dedicated subdomain — NOT on www (the marketing site) and NOT on the app host:

  • Subdomain: config.x.admin_subdomain (config/application.rb) = "admin" in production, "www" in dev/test. Staging overrides it to "admin-staging" via ADMIN_SUBDOMAIN in fly.staging.toml.
  • Routing (config/routes.rb): Avo + Mission Control mount only on the admin subdomain; /admin from any other subdomain 301-redirects there (path preserved).
  • Per-environment setup (both required before deploy): a DNS CNAME (admin[-staging] → <app-name>[-staging].fly.dev) and a Fly TLS cert (fly certs add admin[-staging].example.com --app <app>). Certs issue in ~10–30s once DNS resolves.

GOTCHA 1 — RAILS_HOST is dual-purpose; host authorization only allows subdomains OF it. config/environments/production.rb builds config.hosts from RAILS_HOST:

config.hosts = [ RAILS_HOST, /\A[\w-]+\.RAILS_HOST\z/, "<admin_subdomain>.<apex>", ... ]

Prod RAILS_HOST=app.example.com, so the regex only allows *.app.example.com. admin.example.com is a sibling of app., not a subdomain of it, so it is NOT covered by the regex and Rails returns 403 "Blocked host" unless allowed explicitly. The fix derives the apex from RAILS_HOST (split(".").last(2).join(".")) and adds "<admin_subdomain>.<apex>". Do NOT "fix" this by setting RAILS_HOST=example.com — RAILS_HOST also feeds action_mailer.default_url_options[:host], so that would repoint every magic-link/portal/payment email to the marketing-site apex. Leave RAILS_HOST alone; allow the admin host in config.hosts instead.

GOTCHA 2 — session cookie tld_length must be 2 for a 2-label domain. config/initializers/session_store.rb uses domain: :all. ActionDispatch's cookie code builds the cookie domain by keeping the last tld_length labels of the host via /([^.]+\.?){tld_length}$/ — it does NOT use URL.extract_domain (which has the opposite convention, and is a trap when reasoning about this). So tld_length = the number of labels in the cookie domain itself:

  • tld_length: 2 → .example.com ✓ (shared across app. ↔ admin. ↔ org subdomains — the goal)
  • tld_length: 1 → .com ✗ (a public suffix browsers reject → no session cookie stored → every login silently fails)

actionpack's own docs give the rule: "to share cookies between user1.lvh.me and user2.lvh.me, set :tld_length to 2." example.com and lvh.me are both 2-label domains → tld_length: 2. Verify after any change with curl -sSI https://<host>/ | grep -io 'domain=[^;]*' — it must show domain=example.com, never domain=com.

Post-deploy verification (curl):

curl -sSI https://app.example.com/admin        | grep -iE '^HTTP|^location'  # 301 -> admin.example.com/admin
curl -sSI https://admin.example.com/admin      | grep -iE '^HTTP|^location'  # 302 same-host /users/sign_in (NOT 403 Blocked host)
curl -sSI https://app.example.com/admin/jobs   | grep -iE '^HTTP|^location'  # 301 preserving path
curl -sSI https://app.example.com/             | grep -io 'domain=[^;]*'      # domain=example.com

Changing the session cookie config logs all users out once — expected, not a regression.

Inspect production-only config locally (host authorization + session config only exist in production.rb) without real secrets:

RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 RAILS_HOST=app.example.com \
  bin/rails runner 'Rails.application.config.hosts.each { |h| puts h.inspect }'

Mirror the real env exactly: ENV.fetch("X") { default } only uses the default when the key is absent, not when it is empty — passing ADMIN_SUBDOMAIN= (empty) yields "", not the fallback.

Configuration: fly.toml (Production)

IMPORTANT: Always read the actual fly.toml and fly.staging.toml files for current config. The snippets below may drift from reality. The source of truth is the committed files.

Key configuration notes:

  • THRUSTER_HTTP_PORT = "8080" — Thruster cannot bind privileged port 80 as non-root (uid 1000). Uses 8080 instead.
  • internal_port = 8080 — Must match THRUSTER_HTTP_PORT. Fly proxy routes external 443 → internal 8080.
  • Dockerfile has EXPOSE 8080 to match.
  • Production uses WEB_CONCURRENCY = "2" (cluster Puma), min_machines_running = 1, cpus = 2
  • Staging uses WEB_CONCURRENCY = "0" (single-mode Puma), min_machines_running = 0, cpus = 1

Configuration: fly.staging.toml (Staging)

Key differences from production:

  • WEB_CONCURRENCY = "0" (single-mode Puma, lower memory usage)
  • min_machines_running = 0 (allows full suspend when idle for cost savings)
  • cpus = 1 (shared-cpu-1x for both web and worker)

VM Sizing Reference

ScaleWebWorkerPostgresMediaSoupEst. Cost
~20 active usersshared-cpu-2x, 1GB (2x)shared-cpu-2x, 1GB (1x)MPG Basicshared-cpu-2x, 1GB~$71/mo
~100 active usersshared-cpu-4x, 1GB (2-3x)shared-cpu-2x, 1GB (2x)MPG Launchshared-cpu-4x, 2GB~$375/mo

Environment Variables

Core Rails/tuning: RAILS_ENV, RAILS_MASTER_KEY, SECRET_KEY_BASE, RAILS_HOST, RAILS_LOG_LEVEL, RAILS_MAX_THREADS, WEB_CONCURRENCY, DB_POOL, SQ_DB_POOL, JOB_CONCURRENCY, DEFAULT_ORG_SUBDOMAIN, ADMIN_SUBDOMAIN

  • RAILS_HOST (secret) — the app host. Prod = app.example.com, staging = staging.example.com. Dual-purpose: feeds BOTH config.hosts (host authorization) and mailer link host. fly secrets list only shows names/digests, not values — read the value with fly ssh console --app <app> -C "printenv RAILS_HOST" (a hostname, not sensitive data). See "Platform-admin subdomain" for the host-auth gotcha.
  • ADMIN_SUBDOMAIN — only set on staging (= admin-staging in fly.staging.toml). Unset in production → defaults to admin via config.x.admin_subdomain.

Database: DATABASE_URL (auto-set by MPG attach)

External Services: each third-party integration brings its own secrets — for example STRIPE_SECRET_KEY / STRIPE_WEBHOOK_ENDPOINT_SECRET (payments), MAILGUN_API_KEY (email), HONEYBADGER_API_KEY (error tracking), and MEDIASOUP_SERVER_URL / MEDIASOUP_API_TOKEN / MEDIASOUP_ANNOUNCED_IP (WebRTC). Enumerate the app's actual list with fly secrets list rather than relying on documentation.

Google/GCP (see "Google Cloud Platform" section for details):

  • GOOGLE_CLOUD_PROJECT_ID — GCP project for Vertex AI (<gcp-project>)
  • VERTEX_AI_CREDENTIALS_JSON — service account JSON key as a single-line string (production/staging)
  • VERTEX_AI_CREDENTIALS_FILE — path to a JSON key file (local dev alternative, e.g. tmp/vertex-ai-credentials.json)
  • GCP_PROJECT_ID, GCP_SERVICE_ACCOUNT_EMAIL — GCS/Active Storage (signed URL generation)
  • GCS_RECORDINGS_BUCKET — overrides the recordings bucket name (defaults to <bucket-prefix>-{APP_ENV}-recordings)
  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET — OAuth2 for Google Calendar sync + Meet links
  • GOOGLE_API_KEY — non-OAuth Google API calls
  • APP_ENV — staging/production; determines GCS bucket name suffix

Flyctl Command Reference

Deployment

# Preferred: wrapper that bakes the git SHA into the image and notifies the error tracker
bin/deploy production
bin/deploy staging

# Raw flyctl equivalents (must pass GIT_REVISION yourself; staging MUST use --config)
fly deploy --build-arg GIT_REVISION=$(git rev-parse HEAD)
fly deploy --config fly.staging.toml --build-arg GIT_REVISION=$(git rev-parse HEAD)

# Deploy without running release command (skip migrations)
bin/deploy production --skip-release-command
bin/deploy staging --skip-release-command

# Check what revision is currently deployed
fly ssh console --app <app-name> -C "cat /rails/REVISION"

# View deployment status
fly status --app <app-name>

# View recent releases
fly releases --app <app-name>

# Rollback to previous release
fly releases rollback --app <app-name>

# Rollback to specific version
fly releases rollback --app <app-name> --version <N>

Post-Deploy: Honeybadger Notification

bin/deploy runs this automatically after a successful deploy — the commands below are only needed when deploying with raw fly deploy or re-sending a failed notification.

After every successful deploy, notify Honeybadger (or your error tracker) to track the deployment.

Run it from INSIDE the machine, not locally: HONEYBADGER_API_KEY is a Fly secret and is not in your local shell, so a local bundle exec honeybadger deploy fails with No value provided for required options '--api-key'. Running inside the machine picks up the key from the environment automatically (and keeps the key server-side):

REV=$(git rev-parse HEAD)

# After production deploy
fly ssh console --app <app-name> -C "bundle exec honeybadger deploy \
  --environment production --revision $REV \
  --repository https://github.com/<github-org>/<repo> --user <deploy-user>"

# After staging deploy
fly ssh console --app <app-name>-staging -C "bundle exec honeybadger deploy \
  --environment staging --revision $REV \
  --repository https://github.com/<github-org>/<repo> --user <deploy-user>"

Success prints Deploy notification complete. A [Honeybadger] Rescued an error in a before hook: uninitialized constant Current line is benign — that hook only has data during a real request; the notification still succeeds. (If you must run locally, pass --api-key "$(fly ssh console --app <app-name> -C 'printenv HONEYBADGER_API_KEY')", but prefer running inside the machine.)

Important: Always run this after a successful fly deploy. This enables Honeybadger to correlate errors with specific deployments.

Logs & Debugging

# Tail live logs
fly logs --app <app-name>

# Tail logs for specific process
fly logs --app <app-name> --process web
fly logs --app <app-name> --process worker

# Read production data (Claude runs these; read-only role, no prompt)
echo 'puts User.count' | bin/prod-read
bin/prod-sql -c "select count(*) from users"

# SSH into a running machine (user only — needs the admin token via flyadmin)
flyadmin ssh console --app <app-name>

# SSH and select specific machine
flyadmin ssh console --app <app-name> -s

# Run a shipped rake task on production (user only; dry run → snapshot → apply)
flyadmin ssh console --app <app-name> -C "bin/rails <namespace>:<task>"
flyadmin mpg backup create <cluster-id>
flyadmin ssh console --app <app-name> -C "env APPLY=1 bin/rails <namespace>:<task>"
# Always followed by a go / no-go blurb: the expected final `RESULT:` line of the
# dry run, and what output means stop.

# Check machine status
fly machine list --app <app-name>

# View machine events
fly machine status <machine-id> --app <app-name>

Secrets Management

# List all secrets (names only, values are hidden)
fly secrets list --app <app-name>

# Set a secret (triggers redeploy)
fly secrets set KEY=value --app <app-name>

# Set multiple secrets at once (single redeploy)
fly secrets set KEY1=value1 KEY2=value2 --app <app-name>

# Set secret without redeploying
fly secrets set KEY=value --app <app-name> --stage

# Deploy staged secrets
fly deploy --app <app-name>

# Remove a secret
fly secrets unset KEY --app <app-name>

Scaling

# Scale machine count
fly scale count web=2 worker=1 --app <app-name>

# Scale VM size
fly scale vm shared-cpu-2x --memory 1024 --app <app-name>

# Show current scale
fly scale show --app <app-name>

# Scale specific process group
fly scale count worker=2 --app <app-name>

Managed Postgres

# List MPG clusters
fly mpg list --org <fly-org>

# Connect to Postgres via psql
fly mpg connect <cluster-id>

# View cluster status
fly mpg status <cluster-id>

# Create a backup/snapshot
fly mpg backup create <cluster-id>

# List snapshots
fly mpg backup list <cluster-id>

# Restore from snapshot
fly mpg restore <cluster-id> --backup-id <backup-id>

# Resize Postgres
fly mpg update <cluster-id> --plan <plan-name>

Domains & Certificates

# Add a custom domain
fly certs add app.example.com --app <app-name>

# Check certificate status
fly certs show app.example.com --app <app-name>

# List all certificates
fly certs list --app <app-name>

# Remove a certificate
fly certs remove app.example.com --app <app-name>

# List IP addresses
fly ips list --app <app-name>

# Allocate dedicated IPv4 (needed for apex domains)
fly ips allocate-v4 --app <app-name>

# Allocate IPv6 (free)
fly ips allocate-v6 --app <app-name>

Networking

# List IP addresses
fly ips list --app <app-name>

# Open dashboard
fly dashboard --app <app-name>

# Check app info
fly info --app <app-name>

# View available regions
fly platform regions

Squarespace DNS Configuration

How to Configure DNS at Squarespace

  1. Log in to Squarespace > Domains > Select domain > DNS Settings
  2. Add records as needed (CNAME, A, AAAA, TXT)

Key DNS Rules

  • Apex/root domains (e.g., example.net) cannot use CNAME records per DNS spec. Use A records pointing to Fly.io dedicated IPv4.
  • Subdomains (e.g., app.example.com) should use CNAME records pointing to <app-name>.fly.dev.
  • TTL: Squarespace typically manages TTL automatically. When making changes, expect propagation within 1-48 hours.
  • Verification: Use dig or nslookup to verify DNS propagation:
# Check CNAME resolution
dig app.example.com CNAME +short

# Check A record resolution
dig example.net A +short

# Check full DNS chain
dig app.example.com +trace

# Check from specific DNS server
dig @8.8.8.8 app.example.com

Common DNS Issues

IssueDiagnosisFix
SSL cert not issuingfly certs show <domain> shows pendingDNS not propagated yet, or wrong record type. Wait or fix DNS.
Apex domain not resolvingCNAME on apex domainUse A record with fly ips allocate-v4 dedicated IP instead
Redirect loopSquarespace URL forwarding conflictingRemove any Squarespace forwarding rules for the domain
"DNS not configured" in FlyDNS points elsewhereUpdate Squarespace DNS to point to Fly.io

Google Cloud Platform (GCP)

The application uses GCP for three things: Vertex AI (Gemini) for AI document generation and session transcription, Google Cloud Storage for document/recording file storage (Active Storage), and the Google Calendar API for appointment sync and Meet links. All GCP resources live in a single region (e.g., us-central1 — for regulated data, US data residency), and for HIPAA workloads the project must be covered by Google's BAA with the regulated-data flag enabled.

Repo source-of-truth docs — read these before changing GCP config (if the repository keeps them; adjust paths to your repo):

  • docs/VERTEX_AI_SETUP.md — full Vertex setup, BAA, service account creation, compliance checklist, troubleshooting
  • docs/GCS_BUCKET_SETUP.md — bucket creation, versioning, lifecycle, IAM bindings
  • wiki/google-cloud-platform.md — architecture overview (projects, buckets, service accounts)
  • wiki/environment-variables.md — complete env var reference
  • config/vertex_ai.yml — runtime AI config (model, tokens, timeouts, retry, circuit breaker)

Projects & Service Accounts

ItemValue
Primary GCP project<gcp-project> — Vertex AI + all staging/production buckets
Regionus-central1 for all buckets and Vertex AI calls
Storage service account<service-account>@<gcp-project>.iam.gserviceaccount.com with roles/storage.objectAdmin on the app buckets
Vertex AI auth scopehttps://www.googleapis.com/auth/cloud-platform via service account JSON key
Defunct projectsWatch for old/abandoned GCP projects with billing closed — buckets in them cannot accept writes (accountDisabled errors). Do not point anything at them.

Vertex AI credential resolution order (in the app's Vertex client):

  1. VERTEX_AI_CREDENTIALS_JSON — minified JSON key string (used on Fly.io)
  2. VERTEX_AI_CREDENTIALS_FILE — path to key file (local dev)
  3. Application Default Credentials (ADC) — fallback

GCS credentials come from Rails credentials (Rails.application.credentials.dig(:gcp, :keyfile)) with a dev fallback at config/keys/gcp-storage-key.json.

GCS Buckets (Active Storage)

Two Active Storage services are defined in config/storage.yml:

ServiceBucketHolds
google<bucket-prefix>-{APP_ENV}-documentsDocuments, forms, images, logos, attachments
google_encrypted<bucket-prefix>-{APP_ENV}-recordingsSession recording audio (video meetings + uploads)

Bucket policy summary (details in docs/GCS_BUCKET_SETUP.md):

BucketVersioningLifecycleAccess Logging
<bucket-prefix>-production-documentsYesNone→ <bucket-prefix>-access-logs
<bucket-prefix>-production-recordingsYesNone→ <bucket-prefix>-access-logs
<bucket-prefix>-staging-documentsNoDelete after 90 days→ <bucket-prefix>-access-logs
<bucket-prefix>-staging-recordingsNoDelete after 90 days→ <bucket-prefix>-access-logs
<bucket-prefix>-access-logsNo10-year retention—

All buckets: uniform bucket-level IAM (no per-object ACLs), public access prevention enforced, GCS default AES-256 encryption at rest.

CORS for browser direct uploads: all four app buckets need a CORS policy allowing PUT from the app origins, or Active Storage DirectUpload (bulk-import zips, image uploads, document uploads) fails client-side with Status: 0. Check policies in at config/gcs/cors-{production,staging}.json; apply with gcloud storage buckets update gs://<bucket-name> --cors-file=<file> (full commands + preflight verification in docs/GCS_BUCKET_SETUP.md). Note: an app service account with only objectAdmin cannot read or set bucket CORS — this requires an owner/admin gcloud auth login.

Local dev caveat: Development and test use :local disk storage, not GCS. The transcription pipeline auto-detects this — GCS-backed blobs are passed to Gemini as gs:// URIs; local blobs are sent as inline base64 (subject to Gemini request-size limits, so long audio needs chunking).

Vertex AI (Gemini) Configuration

  • Model: gemini-2.5-flash for both document generation and transcription (set in config/vertex_ai.yml)
  • Endpoint: https://us-central1-aiplatform.googleapis.com/v1/projects/{project}/locations/us-central1/publishers/google/models/{model}:generateContent
  • Client: custom REST client at app/services/vertex_ai/client.rb (Faraday) — no Google AI SDK gem
  • Resilience (all tunable in config/vertex_ai.yml): 3 retries with exponential backoff on 429/500/502/503/504; circuit breaker opens after 5 failures, resets after 60s; 10s connect timeout; 90s read timeout (300s for transcription)
  • Max output tokens: per-document-type, 256 (session summary) up to 65,535 (transcription). Truncated responses surface as finishReason: MAX_TOKENS parse errors — raise the cap for that document type in vertex_ai.yml.
  • Audit trail: every call is logged to an audit model (e.g., AiGenerationLog — model name, status, input/output tokens, duration, error) — query this first when investigating AI cost or failure patterns
  • Consumers: the document-generation service, the transcription service (audio → diarized transcript), and the Generate*Job / Transcribe*Job Solid Queue jobs that wrap them

gcloud CLI Reference

# Auth & project context
gcloud auth login                                        # interactive login (user runs this, not Claude)
gcloud auth activate-service-account SA_EMAIL --key-file=key.json --project=<gcp-project>
gcloud auth list                                         # show active account
gcloud config set project <gcp-project>
gcloud auth print-access-token                           # token for manual curl testing

# Service accounts & keys
gcloud iam service-accounts list --project=<gcp-project>
gcloud iam service-accounts create SA_NAME --project=<gcp-project>
gcloud iam service-accounts keys list --iam-account=SA_EMAIL
gcloud iam service-accounts keys create key.json --iam-account=SA_EMAIL
gcloud iam service-accounts keys delete KEY_ID --iam-account=SA_EMAIL

# IAM roles
gcloud projects get-iam-policy <gcp-project> --flatten="bindings[].members" \
  --filter="bindings.members:SA_EMAIL" --format="table(bindings.role)"
gcloud projects add-iam-policy-binding <gcp-project> \
  --member="serviceAccount:SA_EMAIL" --role="roles/aiplatform.user"
gcloud storage buckets add-iam-policy-binding gs://<bucket-name> \
  --member="serviceAccount:SA_EMAIL" --role="roles/storage.objectAdmin"

# APIs & billing
gcloud services list --enabled --project=<gcp-project>
gcloud services enable aiplatform.googleapis.com storage.googleapis.com --project=<gcp-project>
gcloud billing projects describe <gcp-project>           # check billing is enabled/linked

# Cloud Storage
gcloud storage ls --project=<gcp-project>                          # list buckets
gcloud storage ls gs://<bucket-prefix>-production-recordings/      # list objects
gcloud storage buckets describe gs://<bucket-name>                 # versioning, lifecycle, IAM config

# Smoke-test Vertex AI access with the app's credentials
curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{"contents":[{"role":"user","parts":[{"text":"ping"}]}]}' \
  "https://us-central1-aiplatform.googleapis.com/v1/projects/<gcp-project>/locations/us-central1/publishers/google/models/gemini-2.5-flash:generateContent"

Rotating the Vertex AI Service Account Key

  1. Create the new key: gcloud iam service-accounts keys create new-key.json --iam-account=SA_EMAIL
  2. Minify to one line: jq -c . new-key.json
  3. Set on Fly (staging first): fly secrets set VERTEX_AI_CREDENTIALS_JSON='<minified-json>' --app <app-name>-staging
  4. Verify AI generation works in staging (trigger a document generation, check the AI audit log)
  5. Repeat for production, then delete the old key: gcloud iam service-accounts keys delete OLD_KEY_ID --iam-account=SA_EMAIL
  6. Shred the local key file: rm new-key.json — never commit it

Troubleshooting Playbooks

Deploy Failure

  1. Check the release command output:
    fly logs --app <app-name> | grep "release_command"
  2. Common causes:
    • Migration failure: Check fly logs for ActiveRecord errors. Fix migration, redeploy.
    • Asset compilation failure: Usually a missing dependency. Check Dockerfile.
    • Health check failure: App boots but /up doesn't respond in time. Increase grace_period or fix boot time.
    • Release command timeout: db:prepare hangs or takes too long. Use --wait-timeout 600 for longer timeout. If release already succeeded, redeploy with --skip-release-command.
    • flyctl client timeout: net/http: request canceled means the flyctl client lost connection to Fly API. Use --detach to avoid waiting, then verify manually with fly status.
  3. Recovery:
    # Rollback to last working release
    fly releases rollback --app <app-name>

Machine Config Drift

If fly scale show reports different sizes than fly.toml specifies (e.g., 512MB machines when toml says 1024MB), the existing machines were created with old config. Fly does NOT auto-update existing machines to match toml changes — only new machines get the new config.

Fix: Destroy old machines and redeploy to create fresh ones with correct specs:

fly machine list --app <app>
fly machine destroy <machine-id> --app <app> --force  # for each machine
fly deploy --config fly.staging.toml --detach

App Not Responding / 502 Errors

  1. Check machine status:
    fly status --app <app-name>
    fly machine list --app <app-name>
  2. Check logs for errors:
    fly logs --app <app-name> --process web
  3. Check health endpoint directly:
    curl -I https://app.example.com/up
  4. Common causes:
    • OOM kill: Machine ran out of memory. Scale up VM or reduce WEB_CONCURRENCY.
    • Boot crash: Missing secret or config. Check fly secrets list and logs.
    • Database connection failure: Check MPG status and DATABASE_URL.
    • All machines suspended: If min_machines_running = 0, first request has cold start. Set to 1.

Database Issues

  1. Connection refused:
    # Check MPG cluster status
    fly mpg status <cluster-id>
    # Check if DATABASE_URL is set
    fly secrets list --app <app-name> | grep DATABASE
  2. Slow queries / high load:
    # Connect to database
    fly mpg connect <cluster-id>
    # Check active queries
    SELECT pid, now() - pg_stat_activity.query_start AS duration, query
    FROM pg_stat_activity
    WHERE state != 'idle'
    ORDER BY duration DESC;
  3. Disk space:
    fly mpg status <cluster-id>
    # If nearing limit, resize volume or clean up
  4. Restore from backup:
    fly mpg backup list <cluster-id>
    fly mpg restore <cluster-id> --backup-id <backup-id>

Solid Queue Worker Issues

  1. Jobs not processing:
    # Check worker machine is running
    fly machine list --app <app-name>
    # Check worker logs
    fly logs --app <app-name> --process worker
  2. Jobs backing up:
    • Scale workers: fly scale count worker=2 --app <app-name>
    • Check for stuck/failed jobs via Rails console
  3. Worker OOM:
    • Increase worker memory: Update [[vm]] for worker process in fly.toml
    • Check for memory leaks in job code

SSL Certificate Issues

  1. Certificate not issuing:
    fly certs show <domain> --app <app-name>
    • If "Awaiting configuration": DNS not pointing to Fly.io yet
    • If "Awaiting cert": DNS correct, Let's Encrypt is processing (wait a few minutes)
  2. Certificate expired: Fly.io auto-renews. If stuck:
    fly certs remove <domain> --app <app-name>
    fly certs add <domain> --app <app-name>

Vertex AI / Gemini Failures

Start with the audit trail, not the logs — the AI audit log model records every call:

echo 'puts AiGenerationLog.order(created_at: :desc).limit(10).map { |l| [l.created_at, l.status, l.ai_model_name, l.error_message].join(" | ") }' | bin/prod-read
SymptomCauseFix
AuthenticationError (401/403)Expired/deleted service account key, missing roles/aiplatform.user, or VERTEX_AI_CREDENTIALS_JSON malformed (multi-line JSON)Verify key exists (gcloud iam service-accounts keys list), check IAM role, re-set secret as single-line JSON (jq -c .)
403 SERVICE_DISABLEDaiplatform.googleapis.com not enabledgcloud services enable aiplatform.googleapis.com --project=<gcp-project>
RateLimitError (429)Vertex AI quota exhausted (requests/min or tokens/min per region)Jobs auto-retry with backoff. If persistent: check quotas in GCP Console (IAM > Quotas, filter aiplatform), request increase, or reduce JOB_CONCURRENCY
CircuitOpenError5+ consecutive API failures tripped the circuit breakerFix the underlying error; breaker auto-resets after 60s. Jobs retry with 60s wait
Parse error, finishReason: MAX_TOKENSOutput truncated by the per-document token capRaise max_output_tokens for that document type in config/vertex_ai.yml (transcription already at the 65,535 model max — long audio must be chunked)
Parse error, finishReason: SAFETYGemini safety filter blocked the responseUsually transient/content-specific; check the prompt builder for that document type
Transcription timeoutsLong recordings exceed read timeouttranscription.read_timeout_seconds is 300s in vertex_ai.yml; increase if recordings legitimately run long
billing account ... disabled in state closed on GCS writeBlob is pointed at a bucket in a defunct GCP project with closed billingCheck GCS_RECORDINGS_BUCKET / storage.yml — buckets must live in <gcp-project>. In local dev, use :local storage + inline base64 path instead

Also check the error tracker for errors with external_service: "vertex_ai" context, and verify jobs aren't stuck: transcription/generation runs through Solid Queue (fly logs --app <app-name> --process worker).

GCS / Active Storage Issues

  1. Uploads failing: Check worker/web logs for Google::Cloud::PermissionDeniedError or accountDisabled. Verify the storage service account still has roles/storage.objectAdmin:
    gcloud storage buckets get-iam-policy gs://<bucket-prefix>-production-recordings
  2. Signed URL failures: Requires GCP_SERVICE_ACCOUNT_EMAIL set and the iamcredentials.googleapis.com API enabled (the SA signs URLs via IAM Credentials).
  3. Wrong bucket name: Bucket is derived from APP_ENV (<bucket-prefix>-{APP_ENV}-documents|recordings). If APP_ENV is unset or wrong, Active Storage targets a nonexistent bucket. GCS_RECORDINGS_BUCKET overrides the recordings bucket explicitly.
  4. Never delete objects manually from production buckets — recordings and documents may be regulated data with versioning + audit logging requirements. Restores must preserve the audit trail.

Google Calendar / Meet Sync Issues

  1. Events not syncing: Check the calendar-sync record for the user — expired refresh_token means the user must re-authenticate (OAuth uses prompt: consent + offline access). Token refresh is handled by the app's Google Calendar service (e.g., GoogleCalendarService#refresh_token_if_needed).
  2. invalid_grant errors: Refresh token revoked (user changed Google password, or revoked app access). User must reconnect their Google account.
  3. Meet links missing: Conference creation requires the calendar.events scope; check config/initializers/google_oauth.rb scopes and that the Google Cloud OAuth consent screen still lists them.
  4. OAuth client issues: GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET come from the GCP project's OAuth credentials (APIs & Services > Credentials). Redirect URI must match the app host exactly.

MediaSoup / Video Meeting Issues

  1. Video not connecting:
    • Check MediaSoup app is running: fly status --app <app-name>-mediasoup
    • Verify MEDIASOUP_SERVER_URL and MEDIASOUP_API_TOKEN are set correctly
    • Check MEDIASOUP_ANNOUNCED_IP matches the dedicated IPv4
  2. UDP connectivity:
    • MediaSoup requires UDP ports for WebRTC media
    • Dedicated IPv4 is required: fly ips list --app <app-name>-mediasoup
    • TURN servers (e.g., Twilio) provide fallback for restrictive networks

Memory / Performance Issues

  1. Check current resource usage:
    fly status --app <app-name>
    fly dashboard --app <app-name>  # Opens web dashboard with metrics
  2. OOM kills: Check logs for Out of memory or machine restart events
  3. Tuning:
    • WEB_CONCURRENCY: Number of Puma workers (each ~150-250MB for Rails)
    • RAILS_MAX_THREADS: Threads per worker (each ~50MB)
    • Rule of thumb: WEB_CONCURRENCY * RAILS_MAX_THREADS * ~50MB + base ~200MB < VM memory
    • With 1GB VM and WEB_CONCURRENCY=2, RAILS_MAX_THREADS=5: ~2550 + 200 = 700MB (OK)
  4. jemalloc: Already configured in Dockerfile (libjemalloc2) — reduces memory fragmentation

CI/CD Pipeline

Current Setup

  • CI (ci.yml): Runs on all PRs — linting (StandardRB), security (Brakeman), unit tests (parallel), system tests (parallel with Playwright)
  • Staging deploy: Auto-deploy on push to main after CI passes
  • Production deploy: Manual trigger via workflow_dispatch

GitHub Actions Secrets Required

SecretPurpose
FLY_API_TOKEN_STAGINGDeploy to staging org
FLY_API_TOKEN_PRODUCTIONDeploy to production org

Deploy Workflow Pattern

# Staging: auto on push to main (uses fly.staging.toml)
name: Deploy to Staging
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --config fly.staging.toml
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_STAGING }}
# Production: manual trigger with confirmation (uses fly.toml)
name: Deploy to Production
on:
  workflow_dispatch:
    inputs:
      confirm:
        description: 'Type "deploy" to confirm'
        required: true
jobs:
  deploy:
    if: github.event.inputs.confirm == 'deploy'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PRODUCTION }}

Disaster Recovery Procedures

Database Restore

  1. Identify the snapshot to restore from:
    fly mpg backup list <cluster-id>
  2. Restore to a new cluster (non-destructive):
    fly mpg restore <cluster-id> --backup-id <backup-id> --name <app-name>-db-restored
  3. Verify data in restored cluster
  4. Update app to point to restored cluster:
    fly secrets set DATABASE_URL="<new-connection-string>" --app <app-name>

Full App Recovery

  1. Ensure Fly.io org and app exist
  2. Set all secrets from secure backup
  3. Deploy latest code: fly deploy --app <app-name>
  4. Restore database from snapshot
  5. Verify health check: curl https://app.example.com/up
  6. Verify external service connectivity (payments, email, etc.)

Rollback Procedure

# View recent releases
fly releases --app <app-name>

# Rollback to previous release (keeps current code, reverts to previous image)
fly releases rollback --app <app-name>

# If rollback doesn't fix it, deploy a known-good commit
git checkout <good-commit>
fly deploy --app <app-name>

Planning & Architecture Guidance

When asked to plan infrastructure changes, consider:

  1. Scale projections: Know the current active-user count and the growth target (e.g., 5x in 12 months)
  2. Cost impact: Always estimate monthly cost changes
  3. Compliance implications: Data residency, encryption at rest/transit, BAA coverage (for HIPAA workloads)
  4. Downtime risk: Can this be done with zero downtime? If not, schedule during off-hours
  5. Rollback plan: How to undo if something goes wrong
  6. Staging validation: Test in staging first

Scaling Decision Framework

SignalAction
Response time > 2s consistentlyScale up web machines or increase WEB_CONCURRENCY
Worker job backlog growingScale worker count or increase JOB_CONCURRENCY
Database CPU > 80% sustainedUpgrade MPG plan
Memory usage > 85% on webIncrease VM memory or reduce WEB_CONCURRENCY
OOM kills in logsIncrease VM memory immediately
Cold starts affecting usersEnsure min_machines_running = 1

Thruster Configuration

Thruster (basecamp/thruster) proxies HTTP traffic to Puma and provides asset caching/compression.

  • Version: 0.1.19 (check Gemfile.lock)
  • Port: Listens on THRUSTER_HTTP_PORT (8080), proxies to Puma on TARGET_PORT (3000, default)
  • Why port 8080: Thruster 0.1.19+ cannot bind privileged port 80 as non-root user (the container runs as rails uid 1000). Earlier versions (0.1.13) could, but this broke on upgrade.
  • Configuration: All Thruster env vars can be prefixed with THRUSTER_ to avoid conflicts (e.g., THRUSTER_HTTP_PORT instead of HTTP_PORT)
  • Coordinated config: THRUSTER_HTTP_PORT, internal_port in fly.toml, and EXPOSE in Dockerfile must all match

Deploy Flags Reference

FlagPurpose
--config fly.staging.tomlUse staging config (required for staging deploys)
--skip-release-commandSkip db:prepare (useful when release already succeeded)
--detachReturn immediately instead of monitoring (avoids flyctl client timeouts)
--wait-timeout 600Increase wait time to 10 minutes (default 5m)
--deploy-retries autoAuto-retry failed deployments

Note: The flag is --skip-release-command, NOT --no-release-command (which doesn't exist).

Anti-Patterns to Avoid

  1. Never commit secrets to fly.toml, .env, or any file in git — including GCP service account JSON key files (*.json keys live in config/keys/ locally, which is gitignored, or in Fly secrets / Rails credentials)
  2. Never run destructive database commands without a fresh backup
  3. Never skip staging for infrastructure changes
  4. Never force-push deployment over a failed migration — fix the migration first
  5. Never reduce min_machines_running to 0 in production — cold starts break active user sessions (especially live video meetings)
  6. Never use --skip-release-command in production without understanding migration state
  7. Never expose Rails console access without confirming the user understands the sensitivity of production data
  8. Never change DNS records without documenting the previous values first
  9. Never assume fly scale show matches fly.toml — existing machines keep their old config until destroyed and recreated
  10. Never delete or rotate GCP service account keys without first setting the new key in Fly secrets and verifying — the app authenticates with the exact key in VERTEX_AI_CREDENTIALS_JSON; deleting it breaks all AI generation and transcription immediately
  11. Never move GCP resources out of the approved region — for regulated data everything stays in us-central1 (or your approved region), and for HIPAA workloads <gcp-project> must remain under the Google BAA
  12. Never manually delete objects from production GCS buckets — recordings and documents may be regulated data with versioning and access logging requirements

Execution Strategy

When helping with DevOps tasks:

  1. Gather context - Read fly.toml (production) and fly.staging.toml (staging), check current fly status, review recent logs. For GCP tasks, also read config/vertex_ai.yml, config/storage.yml, and the repo docs listed in the GCP section
  2. Diagnose before acting - Understand the problem fully before proposing changes
  3. Propose a plan - Explain what will happen, what the risks are, and how to rollback
  4. Execute carefully - Reads: run them yourself with bin/prod-read / bin/prod-sql. Writes: hand the user the exact flyadmin / bin/deploy commands in run order, with flyadmin mpg backup create <cluster-id> before the writing step; they run them one at a time. Never bypass the prod-guard hook or restructure commands to dodge it
  5. Verify the outcome - Check health, logs, and metrics after changes
  6. Document changes - Note what was changed and why for future reference

When troubleshooting:

  1. Start with logs - fly logs --app <app-name> is always the first step
  2. Check machine state - fly status and fly machine list
  3. Check health endpoint - curl -I https://app.example.com/up
  4. Check secrets - fly secrets list to verify all required secrets are present
  5. Check database - fly mpg status for the cluster
  6. Isolate the problem - Is it app code, infrastructure, DNS, or an external service?
  7. Fix and verify - Apply fix, verify in logs, check health endpoint

Remember: Production infrastructure supports real users doing real work. Downtime means users can't access their data or attend live video sessions. Always err on the side of caution.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.