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
- antrafa/skillstheir own version, first seen Oct 20, 2026
- felixgeelhaar/skillstheir own version, first seen Jun 4, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- antrafa/skills★ 1same name
- felixgeelhaar/skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository75 other files it gives its agents.
- acceptance-criteriaSkill
- architect-guideSkill
- architect-reviewSkill
- audit-fixerSkill
- avo-admin-expertSkill
- backend-services-expertSkill
- bug-hunt-allSkill
- bug-hunt-fixSkill
- bug-huntSkill
- build-feature-remoteSkill
- build-featureSkill
- codeSkill
- component-testSkill
- controller-expertSkill
- create-qa-documentSkill
- debugSkill
- execute-qaSkill
- feature-qaSkill
- fix-bug-indexSkill
- fix-honeybadgerSkill
- fix-spec-bugsSkill
- fix-system-testSkill
- frontend-expertSkill
- frontend-reviewSkill
- full-qa-fixSkill
- full-qaSkill
- full-reviewSkill
- green-ciSkill
- group-issuesSkill
- honeybadger-auditSkill
- ideateSkill
- impact-assessmentSkill
- improveSkill
- merge-mainSkill
- model-expertSkill
- model-testSkill
- native-bug-hunt-fixSkill
- native-bug-huntSkill
- page-speedSkill
- plan-questionsSkill
- planSkill
- plan-system-testsSkill
- policy-testSkill
- prdSkill
- prd-worktreeSkill
- product-questionSkill
- product-question-tldrSkill
- qa-branchSkill
- qa-testerSkill
- refactor-sweepSkill
- resolve-honeybadgerSkill
- resolve-issueSkill
- resolve-issuesSkill
- review-acceptance-criteriaSkill
- review-fixesSkill
- review-railsSkill
- rspec-test-expertSkill
- scale-reviewSkill
- service-testSkill
- settle-pr-reviewSkill
- The first 60 shown.
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
- Distinctive: line 26
Safety first: - Never run destructive commands without explicit confirmation.
- Distinctive: line 29
Staging first: - Always test changes in staging before production.
- Distinctive: line 30
Observability: - Check logs, metrics, and health checks before and after changes.
- Distinctive: line 31
Reversibility: - Prefer reversible operations.
- Very specific: line 60
Subdomain lookup: If the request has a subdomain (e.g., <org-subdomain>.example.com), the app finds the organization by that subdomain.
- Very specific: line 61
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 …
- Distinctive: line 62
Organization not found: If neither resolves, the user is redirected to an org selection page (root domain) or shown a 404 (invalid …
- Very specific: line 129
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 …
- Distinctive: line 246
After every successful deploy, notify Honeybadger (or your error tracker) to track the deployment.
- Very specific: line 266
Important: Always run this after a successful fly deploy.
- Distinctive: line 411
Add records as needed (CNAME, A, AAAA, TXT)
- Very specific: line 418
Verification: Use dig or nslookup to verify DNS propagation:
- Distinctive: line 490
All buckets: uniform bucket-level IAM (no per-object ACLs), public access prevention enforced, GCS default AES-256 encryption at rest.
- Very specific: line 503
Audit trail: every call is logged to an audit model (e.g., AiGenerationLog — model name, status, input/output tokens, duration, error) — …
- Very specific: line 551
Create the new key: gcloud iam service-accounts keys create new-key.json --iam-account=SA_EMAIL
- Distinctive: line 554
Verify AI generation works in staging (trigger a document generation, check the AI audit log)
- Very specific: line 580
If fly scale show reports different sizes than fly.toml specifies (e.g., 512MB machines when toml says 1024MB), the existing machines were …
- Very specific: line 608
All machines suspended: If min_machines_running = 0, first request has cold start.
- Distinctive: line 651
Check for stuck/failed jobs via Rails console
- Distinctive: line 654
Check for memory leaks in job code
- Distinctive: line 662
If "Awaiting configuration": DNS not pointing to Fly.io yet
- Distinctive: line 663
If "Awaiting cert": DNS correct, Let's Encrypt is processing (wait a few minutes)
- Distinctive: line 672
Start with the audit trail, not the logs — the AI audit log model records every call:
- Very specific: line 692
Uploads failing: Check worker/web logs for Google::Cloud::PermissionDeniedError or accountDisabled.
- Very specific: line 702
Events not syncing: Check the calendar-sync record for the user — expired refresh_token means the user must re-authenticate (OAuth uses …
- Very specific: line 710
Check MediaSoup app is running: fly status --app <app-name>-mediasoup
- Very specific: line 711
Verify MEDIASOUP_SERVER_URL and MEDIASOUP_API_TOKEN are set correctly
- Very specific: line 712
Check MEDIASOUP_ANNOUNCED_IP matches the dedicated IPv4
- Very specific: line 725
OOM kills: Check logs for Out of memory or machine restart events
- Distinctive: line 800
Verify data in restored cluster
- Distinctive: line 801
Update app to point to restored cluster:
- Distinctive: line 808
Ensure Fly.io org and app exist
- Very specific: line 812
Verify health check: curl https://app.example.com/up
- Distinctive: line 813
Verify external service connectivity (payments, email, etc.)
- Distinctive: line 831
When asked to plan infrastructure changes, consider:
- Distinctive: line 834
Cost impact: Always estimate monthly cost changes
- Distinctive: line 838
Staging validation: Test in staging first
- Very specific: line 858
Configuration: All Thruster env vars can be prefixed with THRUSTER_ to avoid conflicts (e.g., THRUSTER_HTTP_PORT instead of HTTP_PORT)
- Distinctive: line 890
When helping with DevOps tasks:
- Very specific: line 892
Gather context: - Read fly.toml (production) and fly.staging.toml (staging), check current fly status, review recent logs.
- Distinctive: line 896
Verify the outcome: - Check health, logs, and metrics after changes
≈ “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.
Files
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:
| Placeholder | Meaning |
<app-name> / <app-name>-staging | Fly.io app names (production / staging) |
<fly-org> / <fly-org>-staging | Fly.io organizations |
<gcp-project> | Google Cloud project ID |
<service-account>@<gcp-project>.iam.gserviceaccount.com | GCP service account email |
<bucket-prefix> | Common prefix for GCS bucket names |
app.example.com / staging.example.com | Production / staging app hosts |
admin.example.com / admin-staging.example.com | Admin-panel hosts |
<github-org>/<repo> | GitHub repository |
Core Principles
- Safety first - Never run destructive commands without explicit confirmation. Always check current state before modifying infrastructure.
- 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.
- Cost awareness - Optimize for cost while maintaining reliability. Use auto-stop/suspend, right-size VMs, and avoid over-provisioning.
- Staging first - Always test changes in staging before production. Verify migrations, config changes, and scaling decisions in staging.
- Observability - Check logs, metrics, and health checks before and after changes. Use an error tracker (e.g., Honeybadger) for error tracking.
- 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 runneras the read-only role) andbin/prod-sql(psql). Use these for every production data question; never proposefly ssh console … bin/rails runnerfor a read. - Writes (deploys, secrets, scaling, rake tasks, restores): the user runs them in their own terminal with
flyadmin(=flywith an admin token kept in the OS keychain) orbin/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 witherrors=0), plus the signals that mean stop (non-zerounmatched/errors, afailed: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):
- Subdomain lookup: If the request has a subdomain (e.g.,
<org-subdomain>.example.com), the app finds the organization by that subdomain. - Default org fallback: If there is no subdomain (custom domain like
app.example.com) or the subdomain iswww, the app falls back to theDEFAULT_ORG_SUBDOMAINenvironment variable. - 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>-stagingTwo 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).
| Environment | Config File | App Name | Org |
| Production | fly.toml | <app-name> | <fly-org> |
| Staging | fly.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>-stagingFly.io Organizations & Apps
| Environment | Org | App Name | Region |
| Production | <fly-org> | <app-name> | sjc (San Jose, CA — pick a region matching your data-residency needs) |
| Staging | <fly-org>-staging | <app-name>-staging | sjc |
| MediaSoup (prod) | <fly-org> | <app-name>-mediasoup | sjc |
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).
| Domain | Type | Record | Target | Purpose |
example.com / www.example.com | — | @ / www | Marketing-site host | Marketing site (NOT the app) |
app.example.com | CNAME | app | <app-name>.fly.dev | Main app (prod) |
admin.example.com | CNAME | admin | <app-name>.fly.dev | Platform-admin /admin (prod) |
staging.example.com | CNAME | staging | <app-name>-staging.fly.dev | Main app (staging) |
admin-staging.example.com | CNAME | admin-staging | <app-name>-staging.fly.dev | Platform-admin /admin (staging) |
example.net (secondary apex, if any) | A | @ | Fly.io dedicated IPv4 | e.g., link shortener |
Verify live hosts with
digrather than trusting docs — hostname conventions drift (e.g.,staging.example.comvsexample-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"viaADMIN_SUBDOMAINinfly.staging.toml. - Routing (
config/routes.rb): Avo + Mission Control mount only on the admin subdomain;/adminfrom 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 acrossapp.↔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.comChanging 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.tomlandfly.staging.tomlfiles 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 matchTHRUSTER_HTTP_PORT. Fly proxy routes external 443 → internal 8080.DockerfilehasEXPOSE 8080to 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
| Scale | Web | Worker | Postgres | MediaSoup | Est. Cost |
| ~20 active users | shared-cpu-2x, 1GB (2x) | shared-cpu-2x, 1GB (1x) | MPG Basic | shared-cpu-2x, 1GB | ~$71/mo |
| ~100 active users | shared-cpu-4x, 1GB (2-3x) | shared-cpu-2x, 1GB (2x) | MPG Launch | shared-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 BOTHconfig.hosts(host authorization) and mailer link host.fly secrets listonly shows names/digests, not values — read the value withfly 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-staginginfly.staging.toml). Unset in production → defaults toadminviaconfig.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 linksGOOGLE_API_KEY— non-OAuth Google API callsAPP_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 regionsSquarespace DNS Configuration
How to Configure DNS at Squarespace
- Log in to Squarespace > Domains > Select domain > DNS Settings
- 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
digornslookupto 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.comCommon DNS Issues
| Issue | Diagnosis | Fix |
| SSL cert not issuing | fly certs show <domain> shows pending | DNS not propagated yet, or wrong record type. Wait or fix DNS. |
| Apex domain not resolving | CNAME on apex domain | Use A record with fly ips allocate-v4 dedicated IP instead |
| Redirect loop | Squarespace URL forwarding conflicting | Remove any Squarespace forwarding rules for the domain |
| "DNS not configured" in Fly | DNS points elsewhere | Update 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, troubleshootingdocs/GCS_BUCKET_SETUP.md— bucket creation, versioning, lifecycle, IAM bindingswiki/google-cloud-platform.md— architecture overview (projects, buckets, service accounts)wiki/environment-variables.md— complete env var referenceconfig/vertex_ai.yml— runtime AI config (model, tokens, timeouts, retry, circuit breaker)
Projects & Service Accounts
| Item | Value |
| Primary GCP project | <gcp-project> — Vertex AI + all staging/production buckets |
| Region | us-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 scope | https://www.googleapis.com/auth/cloud-platform via service account JSON key |
| Defunct projects | Watch 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):
VERTEX_AI_CREDENTIALS_JSON— minified JSON key string (used on Fly.io)VERTEX_AI_CREDENTIALS_FILE— path to key file (local dev)- 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:
| Service | Bucket | Holds |
google | <bucket-prefix>-{APP_ENV}-documents | Documents, forms, images, logos, attachments |
google_encrypted | <bucket-prefix>-{APP_ENV}-recordings | Session recording audio (video meetings + uploads) |
Bucket policy summary (details in docs/GCS_BUCKET_SETUP.md):
| Bucket | Versioning | Lifecycle | Access Logging |
<bucket-prefix>-production-documents | Yes | None | → <bucket-prefix>-access-logs |
<bucket-prefix>-production-recordings | Yes | None | → <bucket-prefix>-access-logs |
<bucket-prefix>-staging-documents | No | Delete after 90 days | → <bucket-prefix>-access-logs |
<bucket-prefix>-staging-recordings | No | Delete after 90 days | → <bucket-prefix>-access-logs |
<bucket-prefix>-access-logs | No | 10-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-flashfor both document generation and transcription (set inconfig/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_TOKENSparse errors — raise the cap for that document type invertex_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*JobSolid 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
- Create the new key:
gcloud iam service-accounts keys create new-key.json --iam-account=SA_EMAIL - Minify to one line:
jq -c . new-key.json - Set on Fly (staging first):
fly secrets set VERTEX_AI_CREDENTIALS_JSON='<minified-json>' --app <app-name>-staging - Verify AI generation works in staging (trigger a document generation, check the AI audit log)
- Repeat for production, then delete the old key:
gcloud iam service-accounts keys delete OLD_KEY_ID --iam-account=SA_EMAIL - Shred the local key file:
rm new-key.json— never commit it
Troubleshooting Playbooks
Deploy Failure
- Check the release command output:
fly logs --app <app-name> | grep "release_command" - Common causes:
- Migration failure: Check
fly logsfor ActiveRecord errors. Fix migration, redeploy. - Asset compilation failure: Usually a missing dependency. Check Dockerfile.
- Health check failure: App boots but
/updoesn't respond in time. Increasegrace_periodor fix boot time. - Release command timeout:
db:preparehangs or takes too long. Use--wait-timeout 600for longer timeout. If release already succeeded, redeploy with--skip-release-command. - flyctl client timeout:
net/http: request canceledmeans the flyctl client lost connection to Fly API. Use--detachto avoid waiting, then verify manually withfly status.
- Migration failure: Check
- 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 --detachApp Not Responding / 502 Errors
- Check machine status:
fly status --app <app-name> fly machine list --app <app-name> - Check logs for errors:
fly logs --app <app-name> --process web - Check health endpoint directly:
curl -I https://app.example.com/up - 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 listand 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.
- OOM kill: Machine ran out of memory. Scale up VM or reduce
Database Issues
- 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 - 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; - Disk space:
fly mpg status <cluster-id> # If nearing limit, resize volume or clean up - Restore from backup:
fly mpg backup list <cluster-id> fly mpg restore <cluster-id> --backup-id <backup-id>
Solid Queue Worker Issues
- Jobs not processing:
# Check worker machine is running fly machine list --app <app-name> # Check worker logs fly logs --app <app-name> --process worker - Jobs backing up:
- Scale workers:
fly scale count worker=2 --app <app-name> - Check for stuck/failed jobs via Rails console
- Scale workers:
- Worker OOM:
- Increase worker memory: Update
[[vm]]for worker process infly.toml - Check for memory leaks in job code
- Increase worker memory: Update
SSL Certificate Issues
- 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)
- 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| Symptom | Cause | Fix |
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_DISABLED | aiplatform.googleapis.com not enabled | gcloud 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 |
CircuitOpenError | 5+ consecutive API failures tripped the circuit breaker | Fix the underlying error; breaker auto-resets after 60s. Jobs retry with 60s wait |
Parse error, finishReason: MAX_TOKENS | Output truncated by the per-document token cap | Raise 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: SAFETY | Gemini safety filter blocked the response | Usually transient/content-specific; check the prompt builder for that document type |
| Transcription timeouts | Long recordings exceed read timeout | transcription.read_timeout_seconds is 300s in vertex_ai.yml; increase if recordings legitimately run long |
billing account ... disabled in state closed on GCS write | Blob is pointed at a bucket in a defunct GCP project with closed billing | Check 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
- Uploads failing: Check worker/web logs for
Google::Cloud::PermissionDeniedErrororaccountDisabled. Verify the storage service account still hasroles/storage.objectAdmin:gcloud storage buckets get-iam-policy gs://<bucket-prefix>-production-recordings - Signed URL failures: Requires
GCP_SERVICE_ACCOUNT_EMAILset and theiamcredentials.googleapis.comAPI enabled (the SA signs URLs via IAM Credentials). - Wrong bucket name: Bucket is derived from
APP_ENV(<bucket-prefix>-{APP_ENV}-documents|recordings). IfAPP_ENVis unset or wrong, Active Storage targets a nonexistent bucket.GCS_RECORDINGS_BUCKEToverrides the recordings bucket explicitly. - 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
- Events not syncing: Check the calendar-sync record for the user — expired
refresh_tokenmeans the user must re-authenticate (OAuth usesprompt: consent+ offline access). Token refresh is handled by the app's Google Calendar service (e.g.,GoogleCalendarService#refresh_token_if_needed). invalid_granterrors: Refresh token revoked (user changed Google password, or revoked app access). User must reconnect their Google account.- Meet links missing: Conference creation requires the
calendar.eventsscope; checkconfig/initializers/google_oauth.rbscopes and that the Google Cloud OAuth consent screen still lists them. - OAuth client issues:
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETcome from the GCP project's OAuth credentials (APIs & Services > Credentials). Redirect URI must match the app host exactly.
MediaSoup / Video Meeting Issues
- Video not connecting:
- Check MediaSoup app is running:
fly status --app <app-name>-mediasoup - Verify
MEDIASOUP_SERVER_URLandMEDIASOUP_API_TOKENare set correctly - Check
MEDIASOUP_ANNOUNCED_IPmatches the dedicated IPv4
- Check MediaSoup app is running:
- 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
- Check current resource usage:
fly status --app <app-name> fly dashboard --app <app-name> # Opens web dashboard with metrics - OOM kills: Check logs for
Out of memoryor machine restart events - 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)
- 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
mainafter CI passes - Production deploy: Manual trigger via
workflow_dispatch
GitHub Actions Secrets Required
| Secret | Purpose |
FLY_API_TOKEN_STAGING | Deploy to staging org |
FLY_API_TOKEN_PRODUCTION | Deploy 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
- Identify the snapshot to restore from:
fly mpg backup list <cluster-id> - Restore to a new cluster (non-destructive):
fly mpg restore <cluster-id> --backup-id <backup-id> --name <app-name>-db-restored - Verify data in restored cluster
- Update app to point to restored cluster:
fly secrets set DATABASE_URL="<new-connection-string>" --app <app-name>
Full App Recovery
- Ensure Fly.io org and app exist
- Set all secrets from secure backup
- Deploy latest code:
fly deploy --app <app-name> - Restore database from snapshot
- Verify health check:
curl https://app.example.com/up - 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:
- Scale projections: Know the current active-user count and the growth target (e.g., 5x in 12 months)
- Cost impact: Always estimate monthly cost changes
- Compliance implications: Data residency, encryption at rest/transit, BAA coverage (for HIPAA workloads)
- Downtime risk: Can this be done with zero downtime? If not, schedule during off-hours
- Rollback plan: How to undo if something goes wrong
- Staging validation: Test in staging first
Scaling Decision Framework
| Signal | Action |
| Response time > 2s consistently | Scale up web machines or increase WEB_CONCURRENCY |
| Worker job backlog growing | Scale worker count or increase JOB_CONCURRENCY |
| Database CPU > 80% sustained | Upgrade MPG plan |
| Memory usage > 85% on web | Increase VM memory or reduce WEB_CONCURRENCY |
| OOM kills in logs | Increase VM memory immediately |
| Cold starts affecting users | Ensure 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 onTARGET_PORT(3000, default) - Why port 8080: Thruster 0.1.19+ cannot bind privileged port 80 as non-root user (the container runs as
railsuid 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_PORTinstead ofHTTP_PORT) - Coordinated config:
THRUSTER_HTTP_PORT,internal_portin fly.toml, andEXPOSEin Dockerfile must all match
Deploy Flags Reference
| Flag | Purpose |
--config fly.staging.toml | Use staging config (required for staging deploys) |
--skip-release-command | Skip db:prepare (useful when release already succeeded) |
--detach | Return immediately instead of monitoring (avoids flyctl client timeouts) |
--wait-timeout 600 | Increase wait time to 10 minutes (default 5m) |
--deploy-retries auto | Auto-retry failed deployments |
Note: The flag is --skip-release-command, NOT --no-release-command (which doesn't exist).
Anti-Patterns to Avoid
- Never commit secrets to
fly.toml,.env, or any file in git — including GCP service account JSON key files (*.jsonkeys live inconfig/keys/locally, which is gitignored, or in Fly secrets / Rails credentials) - Never run destructive database commands without a fresh backup
- Never skip staging for infrastructure changes
- Never force-push deployment over a failed migration — fix the migration first
- Never reduce
min_machines_runningto 0 in production — cold starts break active user sessions (especially live video meetings) - Never use
--skip-release-commandin production without understanding migration state - Never expose Rails console access without confirming the user understands the sensitivity of production data
- Never change DNS records without documenting the previous values first
- Never assume
fly scale showmatchesfly.toml— existing machines keep their old config until destroyed and recreated - 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 - 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 - 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:
- Gather context - Read
fly.toml(production) andfly.staging.toml(staging), check currentfly status, review recent logs. For GCP tasks, also readconfig/vertex_ai.yml,config/storage.yml, and the repo docs listed in the GCP section - Diagnose before acting - Understand the problem fully before proposing changes
- Propose a plan - Explain what will happen, what the risks are, and how to rollback
- Execute carefully - Reads: run them yourself with
bin/prod-read/bin/prod-sql. Writes: hand the user the exactflyadmin/bin/deploycommands in run order, withflyadmin 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 - Verify the outcome - Check health, logs, and metrics after changes
- Document changes - Note what was changed and why for future reference
When troubleshooting:
- Start with logs -
fly logs --app <app-name>is always the first step - Check machine state -
fly statusandfly machine list - Check health endpoint -
curl -I https://app.example.com/up - Check secrets -
fly secrets listto verify all required secrets are present - Check database -
fly mpg statusfor the cluster - Isolate the problem - Is it app code, infrastructure, DNS, or an external service?
- 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.
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
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.

