diff --git a/.claude/CUSTOM-AGENTS-GUIDE.md b/.claude/CUSTOM-AGENTS-GUIDE.md index a8da4e7..86c1d7f 100644 --- a/.claude/CUSTOM-AGENTS-GUIDE.md +++ b/.claude/CUSTOM-AGENTS-GUIDE.md @@ -89,7 +89,7 @@ Integration testing, contract testing, load testing for APIs. ### devops-automator (blue) Docker, CI/CD pipeline generation, deployment automation. -- **When to use:** Setting up Docker, creating CI/CD, deploying to Cloudflare Workers, Node.js, AWS Lambda, or Railway +- **When to use:** Setting up Docker, creating CI/CD, deploying to Cloudflare Workers, Node.js, AWS Lambda, Railway, or Fly.io - **Key capabilities:** Multi-stage Docker, GitHub Actions, wrangler deploy, PM2, health endpoints --- diff --git a/.claude/agents/devops-automator.md b/.claude/agents/devops-automator.md index 6be2c67..edb72ec 100644 --- a/.claude/agents/devops-automator.md +++ b/.claude/agents/devops-automator.md @@ -1,6 +1,6 @@ --- name: devops-automator -description: API deployment and CI/CD specialist for Docker, GitHub Actions, Cloudflare Workers, Node.js, AWS Lambda, and Railway deployments. Use when setting up deployment pipelines, configuring Docker, managing Workers deployments, deploying with SAM, configuring Railway services, or automating build processes. +description: API deployment and CI/CD specialist for Docker, GitHub Actions, Cloudflare Workers, Node.js, AWS Lambda, Railway, and Fly.io deployments. Use when setting up deployment pipelines, configuring Docker, managing Workers deployments, deploying with SAM, configuring Railway services, deploying to Fly.io Machines, or automating build processes. color: blue tools: Bash, Read, Write, Grep, MultiEdit --- diff --git a/.claude/pipeline.config.json b/.claude/pipeline.config.json index 60a9df7..1b7b84f 100644 --- a/.claude/pipeline.config.json +++ b/.claude/pipeline.config.json @@ -68,7 +68,7 @@ "noAnyTypes": true }, "deployment": { - "targets": ["cloudflare-workers", "node-docker", "aws-lambda", "railway"], + "targets": ["cloudflare-workers", "node-docker", "aws-lambda", "railway", "fly"], "defaultTarget": "cloudflare-workers", "cloudflareWorkers": { "compatibilityDate": "2024-12-01", @@ -105,6 +105,17 @@ "managedPostgres": true, "autoDeployFromGitHub": true }, + "fly": { + "builder": "dockerfile", + "internalPort": 3000, + "healthcheckPath": "/health", + "autoStopMachines": "stop", + "autoStartMachines": true, + "minMachinesRunning": 1, + "maxMachines": 3, + "managedPostgres": true, + "autoDeployFromGitHub": false + }, "cicd": { "provider": "github-actions", "runTests": true, diff --git a/.claude/skills/deployment-config-generator/SKILL.md b/.claude/skills/deployment-config-generator/SKILL.md index 26f1140..05b5645 100644 --- a/.claude/skills/deployment-config-generator/SKILL.md +++ b/.claude/skills/deployment-config-generator/SKILL.md @@ -6,12 +6,14 @@ description: > For Node.js, generates Dockerfile and docker-compose.yml with PostgreSQL. For AWS Lambda, generates a SAM template (API Gateway HTTP API), esbuild bundling config, and an OIDC deploy workflow. For Railway, generates railway.toml - (Dockerfile build, health check, start command) and nixpacks.toml. Always - generates GitHub Actions CI/CD workflow and .env.example. Keywords: deploy, - deployment, wrangler, cloudflare, workers, docker, dockerfile, docker-compose, - aws, lambda, sam, api-gateway, serverless, esbuild, oidc, cold-start, railway, - nixpacks, paas, github-actions, ci-cd, pipeline, env, configuration, - infrastructure + (Dockerfile build, health check, start command) and nixpacks.toml. For + Fly.io, generates fly.toml (HTTP service, health check, Machine autoscaling, + Managed Postgres attachment). Always generates GitHub Actions CI/CD workflow + and .env.example. Keywords: deploy, deployment, wrangler, cloudflare, + workers, docker, dockerfile, docker-compose, aws, lambda, sam, api-gateway, + serverless, esbuild, oidc, cold-start, railway, nixpacks, paas, fly, flyio, + flyctl, machines, fly-toml, github-actions, ci-cd, pipeline, env, + configuration, infrastructure --- # Deployment Config Generator (Phase 5) @@ -47,7 +49,7 @@ const spec = JSON.parse( await readFile('.claude/plans/build-spec.json', 'utf-8'), ); -const target = spec.deployment.target; // 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway' +const target = spec.deployment.target; // 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway' | 'fly' const authStrategy = spec.auth.strategy; ``` @@ -410,6 +412,74 @@ cmds = ["pnpm build"] cmd = "node dist/index.js" ``` +### Step 2e -- Fly.io Configuration + +If target is `fly`, reuse the Node.js Dockerfile from Step 2b and generate +`fly.toml`. The canonical template lives in `templates/fly/fly.toml`. Fly.io +builds Machines from the Dockerfile, so the deployed image is identical to +the local `docker compose up` image. + +```toml +# api/fly.toml +app = "my-api" # globally unique; fly launch sets it +primary_region = "iad" + +[build] +dockerfile = "Dockerfile" + +[env] +NODE_ENV = "production" +PORT = "3000" +LOG_LEVEL = "info" + +[http_service] +internal_port = 3000 +force_https = true +auto_stop_machines = "stop" +auto_start_machines = true +min_machines_running = 1 + +[http_service.concurrency] +type = "requests" +soft_limit = 200 +hard_limit = 250 + +[[http_service.checks]] +method = "GET" +path = "/health" +interval = "15s" +timeout = "5s" +grace_period = "10s" + +[[vm]] +size = "shared-cpu-1x" +memory = "512mb" +``` + +Autoscaling is autostop/autostart: Fly stops idle Machines down to +`min_machines_running` and restarts them when traffic crosses the concurrency +`soft_limit`. The ceiling is the number of Machines that exist -- instruct the +user to run `fly scale count 3` for min 1 / max 3. + +Secrets are set with flyctl, not committed (`[env]` holds non-secrets only). +Attaching Managed Postgres sets `DATABASE_URL` automatically: + +```bash +fly mpg create # managed PostgreSQL cluster +fly mpg attach -a # sets DATABASE_URL secret +fly secrets set JWT_SECRET=$(openssl rand -hex 32) +``` + +No deploy workflow is generated by default: `fly deploy` from `api/` builds +and deploys, gated by the `/health` check. Fly.io does not auto-deploy from +GitHub -- if the user wants continuous deployment, point them at the +[GitHub Actions guide](https://fly.io/docs/launch/continuous-deployment-with-github-actions/) +(`flyctl deploy --remote-only` with a `FLY_API_TOKEN` secret). + +Migrations run from the developer's machine through `fly mpg proxy` (the +production image prunes dev dependencies, so a `release_command` migration +would need drizzle-kit added to the image first -- see the fly.toml comments). + ### Step 3 -- Generate GitHub Actions CI/CD ```yaml @@ -514,6 +584,11 @@ jobs: # - run: cd api && pnpm run build:lambda && sam deploy --no-confirm-changeset # For Railway: no deploy step -- Railway auto-deploys the connected # GitHub repo on push, gated by the /health check (see Step 2d). + # For Fly.io (requires a FLY_API_TOKEN secret, see Step 2e): + # - uses: superfly/flyctl-actions/setup-flyctl@master + # - run: cd api && flyctl deploy --remote-only + # env: + # FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} deploy-production: runs-on: ubuntu-latest @@ -592,6 +667,7 @@ Ensure the API package.json has all necessary scripts: | esbuild config | `api/esbuild.config.mjs` | Single-file Lambda bundle build | | Railway config | `api/railway.toml` | Build, health check, start command, restart policy | | Nixpacks config | `api/nixpacks.toml` | Toolchain pins for the non-Docker Railway build | +| Fly.io config | `api/fly.toml` | HTTP service, health check, Machine autoscaling | | CI/CD workflow | `.github/workflows/ci.yml` | GitHub Actions pipeline | | Deploy workflow | `.github/workflows/deploy.yml` | OIDC-based AWS Lambda deploy (Lambda target) | | Env template | `.env.example` | Environment variable template | diff --git a/.claude/skills/schema-intake/SKILL.md b/.claude/skills/schema-intake/SKILL.md index 444b7b8..069764a 100644 --- a/.claude/skills/schema-intake/SKILL.md +++ b/.claude/skills/schema-intake/SKILL.md @@ -84,7 +84,7 @@ continuing. Adapt follow-up questions based on responses. **Question 6 -- Deployment Target** > Where will this API be deployed? -> Options: Cloudflare Workers (default), Node.js / Docker, AWS Lambda, Railway +> Options: Cloudflare Workers (default), Node.js / Docker, AWS Lambda, Railway, Fly.io **Question 7 -- Additional Requirements** (optional) > Any special requirements? (pagination style, rate limiting, soft deletes, etc.) @@ -294,7 +294,7 @@ interface BuildSpec { sourceType: 'file' | 'interview'; }; deployment: { - target: 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway'; + target: 'cloudflare-workers' | 'node' | 'aws-lambda' | 'railway' | 'fly'; runtime: string; }; auth: { @@ -553,7 +553,7 @@ function validateBuildSpec(spec: BuildSpec): string[] { | `tdd-from-schema` (Phase 2) | Reads `endpoints` and `auth` to generate test cases | | `route-generation` (Phase 3) | Reads `endpoints` to scaffold Hono route handlers | | `api-authentication` | Reads `auth` to configure JWT/API key middleware | -| `deployment-config-generator` | Reads `deployment` to generate wrangler.toml, Dockerfile, SAM template, or railway.toml | +| `deployment-config-generator` | Reads `deployment` to generate wrangler.toml, Dockerfile, SAM template, railway.toml, or fly.toml | | `api-documentation` | Reads the generated OpenAPI spec or regenerates from build-spec | ### Triggering the Next Phase diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f719989..45d058d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: fi done - for dir in templates/shared templates/cloudflare-workers templates/node-server templates/docker templates/aws-lambda templates/railway; do + for dir in templates/shared templates/cloudflare-workers templates/node-server templates/docker templates/aws-lambda templates/railway templates/fly; do if [ -d "$dir" ]; then echo " $dir: OK" else diff --git a/CLAUDE.md b/CLAUDE.md index 3d90eb0..3ea98f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a **Claude Code-integrated API & backend development framework** providing specialized agents, skills, scripts, and schema-to-API conversion pipelines. Built with TypeScript, Hono (HTTP framework), Drizzle ORM, and PostgreSQL. Deployed to Cloudflare Workers, Node.js, AWS Lambda, or Railway. +This is a **Claude Code-integrated API & backend development framework** providing specialized agents, skills, scripts, and schema-to-API conversion pipelines. Built with TypeScript, Hono (HTTP framework), Drizzle ORM, and PostgreSQL. Deployed to Cloudflare Workers, Node.js, AWS Lambda, Railway, or Fly.io. Nerva is the backend counterpart to **Aurelius** (frontend framework). @@ -12,7 +12,7 @@ The framework is designed for: - Schema-first API development (OpenAPI spec to working API) - OpenAPI-to-working-API conversion with TDD-mandatory development - Comprehensive testing (integration tests, contract tests, load tests with k6) -- Deployment to Cloudflare Workers, Node.js (Docker), AWS Lambda (SAM), or Railway +- Deployment to Cloudflare Workers, Node.js (Docker), AWS Lambda (SAM), Railway, or Fly.io - Full product lifecycle support (engineering, database, testing, DevOps, operations) ## Project Structure @@ -40,6 +40,7 @@ project-root/ │ ├── node-server/ # Node.js server config │ ├── aws-lambda/ # SAM template, esbuild config, OIDC deploy workflow │ ├── railway/ # Railway config-as-code (railway.toml, nixpacks.toml) +│ ├── fly/ # Fly.io config (fly.toml) │ └── docker/ # Dockerfile, docker-compose ├── docs/ │ ├── schema-to-api/ # Pipeline guide @@ -59,7 +60,7 @@ project-root/ ```bash # Setup a new API project -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway / --fly # Run tests with coverage ./scripts/run-tests.sh @@ -234,7 +235,7 @@ Autonomous 10-phase pipeline that converts an OpenAPI specification into a worki - **Schema-first** — OpenAPI spec drives database schema, types, handlers, and tests - **Contract testing** — generated tests validate API responses against OpenAPI spec - **Load testing** — k6 scripts generated for performance-critical endpoints -- **Multi-target deployment** — Cloudflare Workers, Node.js, Docker, AWS Lambda, Railway with single config change +- **Multi-target deployment** — Cloudflare Workers, Node.js, Docker, AWS Lambda, Railway, Fly.io with single config change - **Type safety end-to-end** — Drizzle schema to Zod validators to TypeScript types - Quality gate: 80%+ coverage, TypeScript strict, security audit, bundle analysis - Resumable: TodoWrite tracks progress across interrupted sessions @@ -491,9 +492,10 @@ pnpm drizzle-kit studio # Open database GUI ./scripts/setup-project.sh my-api --node # New Node.js project ./scripts/setup-project.sh my-api --lambda # New AWS Lambda (SAM) project ./scripts/setup-project.sh my-api --railway # New Railway project +./scripts/setup-project.sh my-api --fly # New Fly.io project ``` --- -**Last Updated:** 2026-06-11 +**Last Updated:** 2026-06-12 **Architecture:** 24 agents, 12 skills, 4 plugins + gh CLI, 9 scripts \ No newline at end of file diff --git a/README.md b/README.md index ff69a53..0fadb6b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A Claude Code-integrated API & backend development framework with TypeScript, Ho - **12 Development Skills** — Automated workflows for schema parsing, TDD, route generation, authentication, validation, documentation - **10-Phase Schema-to-API Pipeline** — Convert OpenAPI specs into fully working, tested API servers with a single command - **TDD-Mandatory Development** — Integration tests written before route handlers, hard gate enforced -- **Four Deployment Targets** — Cloudflare Workers (edge), Node.js (Docker), AWS Lambda (SAM), or Railway (PaaS) with one config change +- **Five Deployment Targets** — Cloudflare Workers (edge), Node.js (Docker), AWS Lambda (SAM), Railway (PaaS), or Fly.io (global containers) with one config change - **Testing Stack** — Vitest, supertest, contract tests against OpenAPI, k6 load tests - **Schema-First Design** — Database schemas, types, and validators all derived from your API spec @@ -37,7 +37,7 @@ cd Nerva pnpm install # Initialize a new API project -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway / --fly # Start development cd api && pnpm dev @@ -177,6 +177,7 @@ Full catalog: [`.claude/CUSTOM-AGENTS-GUIDE.md`](.claude/CUSTOM-AGENTS-GUIDE.md) | `templates/node-server/` | Dockerfile + docker-compose with PostgreSQL | | `templates/aws-lambda/` | SAM template, esbuild config, OIDC deploy workflow | | `templates/railway/` | Railway config-as-code + Nixpacks build config | +| `templates/fly/` | Fly.io config (HTTP service, health check, autoscaling) | | `templates/docker/` | Extended Docker with Redis + pgAdmin | ## Part of the PMDS Framework Series diff --git a/docs/onboarding/architecture.md b/docs/onboarding/architecture.md index 9f37d59..5ad3617 100644 --- a/docs/onboarding/architecture.md +++ b/docs/onboarding/architecture.md @@ -61,7 +61,7 @@ Shell scripts for common development tasks: | Script | Purpose | |--------|---------| -| `setup-project.sh` | Initialize a new API project (--cloudflare, --node, --lambda, or --railway) | +| `setup-project.sh` | Initialize a new API project (--cloudflare, --node, --lambda, --railway, or --fly) | | `run-tests.sh` | Vitest test suite with coverage | | `check-types.sh` | TypeScript strict mode checking | | `security-scan.sh` | Dependency and code security audit | @@ -71,7 +71,7 @@ Shell scripts for common development tasks: | `generate-openapi-docs.sh` | Build API documentation | | `generate-client.sh` | Generate typed API client for frontends | -### Templates (6 directories) +### Templates (7 directories) Starter configurations for different deployment targets: @@ -82,6 +82,7 @@ Starter configurations for different deployment targets: | `templates/node-server/` | Dockerfile + docker-compose with PostgreSQL | | `templates/aws-lambda/` | SAM template (API Gateway HTTP API), esbuild config, OIDC deploy workflow | | `templates/railway/` | Railway config-as-code (Docker build, health check) + Nixpacks config | +| `templates/fly/` | Fly.io config (HTTP service, health check, Machine autoscaling) | | `templates/docker/` | Extended Docker setup with Redis + pgAdmin | ## Pipelines diff --git a/docs/onboarding/quickstart.md b/docs/onboarding/quickstart.md index c95fc2b..67aeda5 100644 --- a/docs/onboarding/quickstart.md +++ b/docs/onboarding/quickstart.md @@ -32,7 +32,7 @@ If you have an OpenAPI specification file: ```bash # Initialize the project structure -./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway +./scripts/setup-project.sh my-api --cloudflare # or --node / --lambda / --railway / --fly # Open Claude Code and run the pipeline /build-from-schema path/to/openapi-spec.yaml diff --git a/docs/schema-to-api/README.md b/docs/schema-to-api/README.md index c2756bb..69814f3 100644 --- a/docs/schema-to-api/README.md +++ b/docs/schema-to-api/README.md @@ -15,7 +15,7 @@ Convert an OpenAPI specification into a fully working, tested API server through | 7 | Validation Layer | Generate Zod schemas for request/response validation | | 8 | Auth & Security | Wire authentication middleware, CORS, rate limiting | | 9 | Documentation | Generate API docs, Swagger UI config, example responses | -| 10 | Deployment Config | Produce Wrangler TOML, Dockerfile, SAM template, or Railway config | +| 10 | Deployment Config | Produce Wrangler TOML, Dockerfile, SAM template, Railway config, or Fly.io config | ## Phase-by-Phase Deep Dive @@ -102,12 +102,12 @@ Convert an OpenAPI specification into a fully working, tested API server through ### Phase 10: Deployment Config -**What it does:** Produces deployment configuration files matching the target platform. For Cloudflare Workers: `wrangler.toml` with D1 bindings. For Node.js: `Dockerfile` with multi-stage build and `docker-compose.yml`. For AWS Lambda: `template.yaml` (SAM with an API Gateway HTTP API), `samconfig.toml`, esbuild bundle config, and an OIDC-based deploy workflow. For Railway: `railway.toml` (Dockerfile build, health check, start command) and `nixpacks.toml`. +**What it does:** Produces deployment configuration files matching the target platform. For Cloudflare Workers: `wrangler.toml` with D1 bindings. For Node.js: `Dockerfile` with multi-stage build and `docker-compose.yml`. For AWS Lambda: `template.yaml` (SAM with an API Gateway HTTP API), `samconfig.toml`, esbuild bundle config, and an OIDC-based deploy workflow. For Railway: `railway.toml` (Dockerfile build, health check, start command) and `nixpacks.toml`. For Fly.io: `fly.toml` (HTTP service, health check, Machine autoscaling). - **Inputs:** Project structure, database config, environment variables -- **Outputs:** `wrangler.toml`, `Dockerfile` + `docker-compose.yml`, `template.yaml` + `samconfig.toml` + `esbuild.config.mjs`, or `railway.toml` + `nixpacks.toml` +- **Outputs:** `wrangler.toml`, `Dockerfile` + `docker-compose.yml`, `template.yaml` + `samconfig.toml` + `esbuild.config.mjs`, `railway.toml` + `nixpacks.toml`, or `fly.toml` - **Tools:** `deployment-config-generator` skill -- **Config:** `pipeline.config.json` -- `deployment.target` (cloudflare, node, aws-lambda, railway, both) +- **Config:** `pipeline.config.json` -- `deployment.target` (cloudflare, node, aws-lambda, railway, fly, both) ## Configuration diff --git a/package.json b/package.json index f7d6cec..445e0a9 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "tdd", "cloudflare-workers", "aws-lambda", - "railway" + "railway", + "flyio" ], "author": "PAMulligan", "license": "MIT", diff --git a/scripts/setup-project.sh b/scripts/setup-project.sh index ae9c15c..cb064ec 100644 --- a/scripts/setup-project.sh +++ b/scripts/setup-project.sh @@ -3,7 +3,7 @@ set -euo pipefail # ============================================================================ # setup-project.sh - Initialize a new Nerva API project -# Usage: ./scripts/setup-project.sh [--cloudflare|--node|--lambda|--railway] [--dry-run] +# Usage: ./scripts/setup-project.sh [--cloudflare|--node|--lambda|--railway|--fly] [--dry-run] # ============================================================================ RED='\033[0;31m' @@ -26,11 +26,12 @@ TEMPLATES_DIR="$PROJECT_ROOT/templates" if [[ $# -lt 1 ]]; then error "Missing project name." - echo "Usage: $0 [--cloudflare|--node|--lambda|--railway] [--dry-run]" + echo "Usage: $0 [--cloudflare|--node|--lambda|--railway|--fly] [--dry-run]" echo " --cloudflare Set up for Cloudflare Workers deployment" echo " --node Set up for Node.js / Docker deployment (default)" echo " --lambda Set up for AWS Lambda deployment (SAM + API Gateway HTTP API)" echo " --railway Set up for Railway deployment (Docker build + managed PostgreSQL)" + echo " --fly Set up for Fly.io deployment (Docker build + Fly Machines + managed PostgreSQL)" echo " --dry-run Preview what would be created without making changes" exit 1 fi @@ -46,6 +47,7 @@ while [[ $# -gt 0 ]]; do --node) PLATFORM="node"; shift ;; --lambda) PLATFORM="lambda"; shift ;; --railway) PLATFORM="railway"; shift ;; + --fly) PLATFORM="fly"; shift ;; --dry-run) DRY_RUN=true; shift ;; *) error "Unknown option: $1"; exit 1 ;; esac @@ -325,6 +327,34 @@ RENVEOF fi success "Railway configured. Connect the repo in Railway and set Root Directory to 'api'." +elif [[ "$PLATFORM" == "fly" ]]; then + step "Setting up Fly.io..." + # Fly.io builds the same multi-stage image as the Node.js target + copy_file "$TEMPLATES_DIR/node-server/Dockerfile" "$API_DIR/Dockerfile" + copy_file "$TEMPLATES_DIR/node-server/docker-compose.yml" "$API_DIR/docker-compose.yml" + copy_file "$TEMPLATES_DIR/fly/fly.toml" "$API_DIR/fly.toml" + + write_file "$API_DIR/.env.example" << 'FENVEOF' +# Local development values. On Fly.io, non-secret config lives in the [env] +# section of fly.toml; secrets are set with flyctl and override [env]: +# fly secrets set JWT_SECRET=$(openssl rand -hex 32) +# Attaching Managed Postgres (fly mpg attach) sets DATABASE_URL automatically. +NODE_ENV=development +PORT=3000 +DATABASE_URL=postgresql://nerva:nerva_secret@localhost:5432/nerva_db +LOG_LEVEL=debug +APP_VERSION=0.0.1 +HEALTH_DB_TIMEOUT_MS=2000 +# Required in production (min 32 chars). Generate with: openssl rand -hex 32 +JWT_SECRET=dev-secret-change-me-in-production-min-32-chars +FENVEOF + + if ! command -v fly &>/dev/null && ! command -v flyctl &>/dev/null; then + warn "Fly.io CLI (flyctl) not found. Install it before deploying:" + warn " https://fly.io/docs/flyctl/install/" + fi + + success "Fly.io configured. Run 'fly launch --no-deploy' from api/ to set the app name in fly.toml." else step "Setting up Node.js / Docker..." copy_file "$TEMPLATES_DIR/node-server/Dockerfile" "$API_DIR/Dockerfile" @@ -359,6 +389,10 @@ elif [[ "$PLATFORM" == "railway" ]]; then DEV_BASE_URL="http://localhost:3000" PLATFORM_LABEL="Railway" DEV_STEPS=$'docker compose up -d # start PostgreSQL\npnpm dev # start the dev server' +elif [[ "$PLATFORM" == "fly" ]]; then + DEV_BASE_URL="http://localhost:3000" + PLATFORM_LABEL="Fly.io" + DEV_STEPS=$'docker compose up -d # start PostgreSQL\npnpm dev # start the dev server' else DEV_BASE_URL="http://localhost:3000" PLATFORM_LABEL="Node.js / Docker" @@ -638,6 +672,80 @@ To build without the Dockerfile, set `builder = "NIXPACKS"` in build/start commands. The Dockerfile build stays the default because it ships the exact image you can test locally. READMERAILWAY + elif [[ "$PLATFORM" == "fly" ]]; then + cat << 'READMEFLY' + +## Deploying to Fly.io + +The API deploys to [Fly.io](https://fly.io) as Machines built from +`api/Dockerfile` (the same hardened multi-stage image as the Node.js target), +configured by `api/fly.toml` -- so what deploys is exactly what +`docker compose up` runs locally. + +### One-time setup + +1. Install [flyctl](https://fly.io/docs/flyctl/install/) and sign in with + `fly auth login`. +2. Create the app from `api/` -- `fly launch` adopts the existing `fly.toml`, + prompts for a globally unique app name, and writes it back: + + ```bash + cd api + fly launch --no-deploy + ``` + +3. Create and attach [Managed Postgres](https://fly.io/docs/mpg/). Attaching + sets the `DATABASE_URL` secret on the app automatically: + + ```bash + fly mpg create + fly mpg attach -a + ``` + +4. Set the remaining secrets (non-secret config lives in the `[env]` section + of `fly.toml`): + + ```bash + fly secrets set JWT_SECRET=$(openssl rand -hex 32) + ``` + +### Deploys + +```bash +fly deploy # build and deploy from api/ +``` + +Each deploy must answer on `/health` (`[[http_service.checks]]` in `fly.toml`) +before Machines take traffic; a failing check keeps the previous version live. +Fly.io does not auto-deploy from GitHub -- for continuous deployment, add a +[GitHub Actions workflow](https://fly.io/docs/launch/continuous-deployment-with-github-actions/) +that runs `flyctl deploy --remote-only` with a `FLY_API_TOKEN` secret. + +### Scaling + +`fly.toml` keeps at least one Machine running (`min_machines_running = 1`) and +stops idle Machines; stopped Machines restart automatically on incoming +traffic. The ceiling is the number of Machines that exist, so create three for +min 1 / max 3 autoscaling: + +```bash +fly scale count 3 +``` + +### Migrations + +Run database migrations against the Fly.io database from your machine through +a local proxy: + +```bash +fly mpg proxy # forwards a local port to the database +DATABASE_URL= pnpm db:migrate +``` + +A `release_command` in `fly.toml` could run migrations on each deploy instead, +but the production image prunes dev dependencies (no drizzle-kit), so the +proxy approach is the default -- see the comments in `fly.toml`. +READMEFLY fi cat << 'READMESTATIC' @@ -713,6 +821,11 @@ if ! $DRY_RUN; then echo " pnpm dev # Start dev server" echo " railway init # Create Railway project (or connect GitHub)" echo " railway up # Deploy from the CLI" + elif [[ "$PLATFORM" == "fly" ]]; then + echo " docker compose up -d # Start PostgreSQL" + echo " pnpm dev # Start dev server" + echo " fly launch --no-deploy # Create the Fly.io app (adopts fly.toml)" + echo " fly deploy # Deploy to Fly.io" else echo " docker compose up -d # Start PostgreSQL" echo " pnpm dev # Start dev server" diff --git a/templates/fly/fly.toml b/templates/fly/fly.toml new file mode 100644 index 0000000..7d1c446 --- /dev/null +++ b/templates/fly/fly.toml @@ -0,0 +1,63 @@ +# Nerva API - Fly.io configuration +# https://fly.io/docs/reference/configuration/ +# +# Fly.io builds this app from the Dockerfile -- the same hardened multi-stage +# image used by the Node.js target -- so what deploys is exactly what +# `docker compose up` runs locally. +# +# Replace the app name below before deploying. App names are globally unique; +# running `fly launch --no-deploy` from api/ adopts this file, prompts for a +# name, and writes it back. + +app = "my-api" +primary_region = "iad" + +[build] +dockerfile = "Dockerfile" + +# Non-secret configuration. Secrets (JWT_SECRET, DATABASE_URL) are set with +# `fly secrets set` and override [env] values. Attaching Managed Postgres +# (fly mpg attach) sets the DATABASE_URL secret automatically: +# https://fly.io/docs/mpg/ +[env] +NODE_ENV = "production" +PORT = "3000" +LOG_LEVEL = "info" + +[deploy] +# Run migrations before each deploy goes live. Requires drizzle-kit inside the +# image -- the production Dockerfile prunes dev dependencies, so run +# migrations from your machine through `fly mpg proxy` instead, or add +# drizzle-kit to the production image before uncommenting: +# release_command = "pnpm db:migrate" + +[http_service] +internal_port = 3000 +force_https = true +# Autoscaling: Fly stops idle Machines down to min_machines_running and +# restarts them on incoming traffic, up to the number of Machines that exist. +# Create the ceiling with `fly scale count 3` for min 1 / max 3: +# https://fly.io/docs/launch/autostop-autostart/ +auto_stop_machines = "stop" +auto_start_machines = true +min_machines_running = 1 + +# Per-Machine request limits; crossing soft_limit is what triggers Fly to +# start a stopped Machine. +[http_service.concurrency] +type = "requests" +soft_limit = 200 +hard_limit = 250 + +# Each deploy must answer on /health before Machines take traffic; a failing +# check keeps the previous version live. +[[http_service.checks]] +method = "GET" +path = "/health" +interval = "15s" +timeout = "5s" +grace_period = "10s" + +[[vm]] +size = "shared-cpu-1x" +memory = "512mb"