Skip to main content

Fly.io Deployment

Deploy ProjectAchilles to Fly.io using Docker Machines with persistent volumes. At ~$8/month, this is the cheapest always-on deployment option.

Architecture​

ServiceMachine SizeVolumePublic Domain
achilles-backendshared-2x, 512 MB1 GB achilles_dataYes
achilles-frontendshared-1x, 256 MBNoneYes

The frontend calls the backend directly via CORS. Both Machines run always-on (auto_stop_machines = 'off') since agents send heartbeats every 60s.

Prerequisites​

  • Fly.io account
  • flyctl CLI: curl -L https://fly.io/install.sh | sh
  • Clerk application keys
  • Elastic Cloud deployment

Step 1: Create Apps and Volume​

flyctl auth login

# Create apps
flyctl apps create achilles-backend --org personal
flyctl apps create achilles-frontend --org personal

# Create persistent volume (must match region in fly.toml)
flyctl volumes create achilles_data --app achilles-backend --region cdg --size 1

Step 2: Set Environment Variables​

flyctl secrets set \
CLERK_PUBLISHABLE_KEY="pk_live_..." \
CLERK_SECRET_KEY="sk_live_..." \
SESSION_SECRET="$(openssl rand -base64 32)" \
ENCRYPTION_SECRET="$(openssl rand -base64 32)" \
CLI_AUTH_SECRET="$(openssl rand -base64 32)" \
CORS_ORIGIN="https://achilles-frontend.fly.dev" \
AGENT_SERVER_URL="https://achilles-backend.fly.dev" \
TESTS_REPO_URL="https://github.com/ubercylon8/f0_library.git" \
GITHUB_TOKEN="ghp_..." \
ELASTICSEARCH_CLOUD_ID="..." \
ELASTICSEARCH_API_KEY="..." \
--app achilles-backend

flyctl secrets set \
CLERK_PUBLISHABLE_KEY="pk_live_..." \
VITE_API_URL="https://achilles-backend.fly.dev" \
--app achilles-frontend

Step 3: Deploy​

cd backend && flyctl deploy --app achilles-backend
cd ../frontend && flyctl deploy --app achilles-frontend

First builds take 3-5 minutes. Subsequent builds reuse cached Docker layers.

Step 4: Verify​

curl https://achilles-backend.fly.dev/api/health
# Expected: {"status":"ok","service":"ProjectAchilles",...}

Visit https://achilles-frontend.fly.dev and verify Clerk login works.

Custom Domains​

Fly.io uses A/AAAA records (not CNAME like Render/Vercel):

flyctl certs create your-frontend-domain --app achilles-frontend
flyctl certs create your-backend-domain --app achilles-backend

Add the A/AAAA records shown by flyctl certs show to your DNS provider. TLS certificates are auto-provisioned by Let's Encrypt.

Deploying Updates​

Fly.io does not auto-deploy from GitHub. Either deploy manually or set up CI/CD:

# .github/workflows/deploy-fly.yml
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --app achilles-backend
working-directory: backend
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Useful Commands​

flyctl logs --app achilles-backend          # View logs
flyctl ssh console --app achilles-backend # SSH into Machine
flyctl status --app achilles-backend # Machine status
flyctl volumes list --app achilles-backend # List volumes
flyctl apps restart achilles-backend # Restart

Agent Build from Source​

Full Go build support (Docker image includes Go 1.24.3). Set AGENT_REPO_URL and AGENT_REPO_BRANCH.

Before every build, the backend fetches the latest commit of AGENT_REPO_BRANCH (git fetch --depth 1 + hard reset), so a build always compiles the current branch, even when an agent-only merge didn't trigger a redeploy. The commit is recorded in the version's release notes (Built from source (windows/amd64) at 1bbe749). If the fetch fails, the build fails rather than compiling stale code.

Cost Estimate​

ServiceEst. Monthly Cost
Backend (shared-2x, 512 MB)~$5
Frontend (shared-1x, 256 MB)~$3
Volume (1 GB)~$0.15
Total~$8

Helper Scripts​

# Generate all secrets in flyctl format (copy-paste ready)
./scripts/generate-secrets.sh --target fly --format flyctl

# Interactive setup wizard — collects Clerk/ES config, outputs Fly.io-ready env vars
./scripts/setup.sh # Select: PaaS → Fly.io

# Initialize Elasticsearch indices on Elastic Cloud
./scripts/init-elasticsearch.sh --cloud-id "deploy:..." --api-key "..."

Troubleshooting​

Frontend shows "502 Bad Gateway" at startup​

The nginx config hardcodes Docker Compose hostnames. Set VITE_API_URL on the frontend — docker-entrypoint.sh detects this and removes the proxy blocks.

Volume data not persisting​

Verify the volume is attached and in the same region as the Machine:

flyctl volumes list --app achilles-backend

TLS certificate not provisioning​

flyctl certs check <domain> --app <app-name>

Verify DNS records point to the correct Fly.io IPs.