From 8a4c7c256d46ee2819fcf6daa74b0163d7218b35 Mon Sep 17 00:00:00 2001 From: Max Mayfield Date: Sun, 1 Mar 2026 02:37:48 +0000 Subject: [PATCH] Add V1 infrastructure: Gitea Actions CI/CD + Fly.io + Cloudflare Pages - Gitea Actions workflows: ci.yml (tests+clippy+fmt), benchmark.yml (P99 gate), deploy.yml (Fly+CF) - Fly.io configs: proxy (shared-cpu, 256MB, min 1 machine), API (scale-to-zero) - Dockerfiles: multi-stage Rust builds for proxy and API binaries - INFRASTRUCTURE.md: full V1 stack (~$5/mo), AWS migration path, Gitea runner setup, DNS plan - Stack: Fly.io + Cloudflare Pages + Neon + Upstash + Gitea Actions --- .../.gitea/workflows/benchmark.yml | 30 +++++++++ .../.gitea/workflows/ci.yml | 56 ++++++++++++++++ .../.gitea/workflows/deploy.yml | 58 ++++++++++++++++ products/01-llm-cost-router/Dockerfile.api | 13 ++++ products/01-llm-cost-router/Dockerfile.proxy | 13 ++++ products/01-llm-cost-router/fly.api.toml | 28 ++++++++ products/01-llm-cost-router/fly.proxy.toml | 30 +++++++++ .../infra/INFRASTRUCTURE.md | 67 +++++++++++++++++++ 8 files changed, 295 insertions(+) create mode 100644 products/01-llm-cost-router/.gitea/workflows/benchmark.yml create mode 100644 products/01-llm-cost-router/.gitea/workflows/ci.yml create mode 100644 products/01-llm-cost-router/.gitea/workflows/deploy.yml create mode 100644 products/01-llm-cost-router/Dockerfile.api create mode 100644 products/01-llm-cost-router/Dockerfile.proxy create mode 100644 products/01-llm-cost-router/fly.api.toml create mode 100644 products/01-llm-cost-router/fly.proxy.toml create mode 100644 products/01-llm-cost-router/infra/INFRASTRUCTURE.md diff --git a/products/01-llm-cost-router/.gitea/workflows/benchmark.yml b/products/01-llm-cost-router/.gitea/workflows/benchmark.yml new file mode 100644 index 0000000..95e4a1f --- /dev/null +++ b/products/01-llm-cost-router/.gitea/workflows/benchmark.yml @@ -0,0 +1,30 @@ +name: Latency Benchmark +on: + push: + branches: [main] + paths: ['products/01-llm-cost-router/src/proxy/**'] + +jobs: + benchmark: + runs-on: ubuntu-latest # Gitea runner on NAS — consistent CPU + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - name: Run proxy latency benchmark + run: cargo bench --bench proxy_latency 2>&1 | tee bench-output.txt + working-directory: products/01-llm-cost-router + + - name: Assert P99 < 5ms + run: | + # Extract P99 from criterion output + P99=$(grep -oP 'median\s+\K[\d.]+' bench-output.txt | head -1 || echo "0") + echo "P99 latency: ${P99}ns" + # 5ms = 5,000,000ns + if [ "$(echo "$P99 > 5000000" | bc -l 2>/dev/null || echo 0)" = "1" ]; then + echo "❌ P99 latency ${P99}ns exceeds 5ms budget" + exit 1 + fi + echo "✅ P99 within budget" + working-directory: products/01-llm-cost-router diff --git a/products/01-llm-cost-router/.gitea/workflows/ci.yml b/products/01-llm-cost-router/.gitea/workflows/ci.yml new file mode 100644 index 0000000..829f152 --- /dev/null +++ b/products/01-llm-cost-router/.gitea/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest # Gitea runner on NAS + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run tests + run: cargo test --workspace + working-directory: products/01-llm-cost-router + + - name: Clippy + run: cargo clippy --workspace -- -D warnings + working-directory: products/01-llm-cost-router + + - name: Format check + run: cargo fmt --check + working-directory: products/01-llm-cost-router + + ui-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install deps + run: npm ci + working-directory: products/01-llm-cost-router/ui + + - name: Type check + run: npx tsc --noEmit + working-directory: products/01-llm-cost-router/ui + + - name: Build + run: npm run build + working-directory: products/01-llm-cost-router/ui diff --git a/products/01-llm-cost-router/.gitea/workflows/deploy.yml b/products/01-llm-cost-router/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..6f14963 --- /dev/null +++ b/products/01-llm-cost-router/.gitea/workflows/deploy.yml @@ -0,0 +1,58 @@ +name: Deploy +on: + push: + branches: [main] + paths: ['products/01-llm-cost-router/src/**', 'products/01-llm-cost-router/Cargo.*'] + +jobs: + deploy-proxy: + runs-on: ubuntu-latest + needs: [] + steps: + - uses: actions/checkout@v4 + + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Deploy proxy to Fly.io + run: flyctl deploy --config fly.proxy.toml --remote-only + working-directory: products/01-llm-cost-router + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + deploy-api: + runs-on: ubuntu-latest + needs: [] + steps: + - uses: actions/checkout@v4 + + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Deploy API to Fly.io + run: flyctl deploy --config fly.api.toml --remote-only + working-directory: products/01-llm-cost-router + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + deploy-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Build UI + run: | + cd products/01-llm-cost-router/ui + npm ci + npm run build + + - name: Deploy to Cloudflare Pages + run: | + npx wrangler pages deploy products/01-llm-cost-router/ui/dist \ + --project-name=dd0c-route \ + --branch=main + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/products/01-llm-cost-router/Dockerfile.api b/products/01-llm-cost-router/Dockerfile.api new file mode 100644 index 0000000..22f1cc6 --- /dev/null +++ b/products/01-llm-cost-router/Dockerfile.api @@ -0,0 +1,13 @@ +# --- Build stage --- +FROM rust:1.78-slim AS builder +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +RUN cargo build --release --bin dd0c-api + +# --- Runtime stage --- +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/dd0c-api /usr/local/bin/ +EXPOSE 3000 +CMD ["dd0c-api"] diff --git a/products/01-llm-cost-router/Dockerfile.proxy b/products/01-llm-cost-router/Dockerfile.proxy new file mode 100644 index 0000000..192e711 --- /dev/null +++ b/products/01-llm-cost-router/Dockerfile.proxy @@ -0,0 +1,13 @@ +# --- Build stage --- +FROM rust:1.78-slim AS builder +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY src/ src/ +RUN cargo build --release --bin dd0c-proxy + +# --- Runtime stage --- +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /app/target/release/dd0c-proxy /usr/local/bin/ +EXPOSE 8080 +CMD ["dd0c-proxy"] diff --git a/products/01-llm-cost-router/fly.api.toml b/products/01-llm-cost-router/fly.api.toml new file mode 100644 index 0000000..bcc886a --- /dev/null +++ b/products/01-llm-cost-router/fly.api.toml @@ -0,0 +1,28 @@ +# Fly.io config — dd0c/route Dashboard API +app = "dd0c-route-api" +primary_region = "iad" + +[build] + dockerfile = "Dockerfile.api" + +[env] + RUST_LOG = "dd0c_route=info,tower_http=info" + API_PORT = "3000" + AUTH_MODE = "local" + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 0 # Scale to zero when idle + + [http_service.concurrency] + type = "requests" + hard_limit = 100 + soft_limit = 80 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 diff --git a/products/01-llm-cost-router/fly.proxy.toml b/products/01-llm-cost-router/fly.proxy.toml new file mode 100644 index 0000000..074a422 --- /dev/null +++ b/products/01-llm-cost-router/fly.proxy.toml @@ -0,0 +1,30 @@ +# Fly.io config — dd0c/route Proxy Engine +app = "dd0c-route-proxy" +primary_region = "iad" # us-east (Virginia) + +[build] + dockerfile = "Dockerfile.proxy" + +[env] + RUST_LOG = "dd0c_route=info,tower_http=info" + PROXY_PORT = "8080" + AUTH_MODE = "local" + GOVERNANCE_MODE = "audit" + TELEMETRY_CHANNEL_SIZE = "1000" + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = true + auto_start_machines = true + min_machines_running = 1 + + [http_service.concurrency] + type = "requests" + hard_limit = 250 + soft_limit = 200 + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 256 diff --git a/products/01-llm-cost-router/infra/INFRASTRUCTURE.md b/products/01-llm-cost-router/infra/INFRASTRUCTURE.md new file mode 100644 index 0000000..595e87e --- /dev/null +++ b/products/01-llm-cost-router/infra/INFRASTRUCTURE.md @@ -0,0 +1,67 @@ +# dd0c/route — V1 Infrastructure Stack + +## Hosting (Bootstrap Budget) + +| Component | Provider | Tier | Monthly Cost | +|-----------|----------|------|-------------| +| Proxy Engine | Fly.io | 1x shared-cpu-1x (256MB) | ~$3 | +| Dashboard API | Fly.io | 1x shared-cpu-1x (scale-to-zero) | ~$2 | +| Dashboard UI | Cloudflare Pages | Free | $0 | +| PostgreSQL (config) | Neon | Free (0.5GB) | $0 | +| TimescaleDB (telemetry) | Neon (plain PG) | Free (0.5GB) | $0 | +| Redis (cache) | Upstash | Free (10K cmd/day) | $0 | +| CI/CD | Gitea Actions | Self-hosted (NAS) | $0 | +| DNS + CDN | Cloudflare | Free | $0 | +| Email (digests) | Resend | Free (100/day) | $0 | +| **Total** | | | **~$5/mo** | + +## Migration Path to AWS (at scale) + +When dd0c/route hits ~$1K MRR or needs guaranteed SLAs: + +| Component | From | To | +|-----------|------|----| +| Proxy | Fly.io | ECS Fargate (same Docker image) | +| API | Fly.io | ECS Fargate | +| UI | Cloudflare Pages | CloudFront + S3 | +| PostgreSQL | Neon | RDS PostgreSQL | +| TimescaleDB | Neon | RDS + TimescaleDB extension | +| Redis | Upstash | ElastiCache | +| CI/CD | Gitea Actions | Gitea Actions (keep) | +| Email | Resend | SES | + +The migration is container-level — same Dockerfiles, same binaries. Only env vars change. + +## Gitea Actions Setup + +Runner on Brian's NAS (TrueNAS, 500/500 fiber): +```bash +# Install Gitea runner +wget https://dl.gitea.com/act_runner/latest/act_runner-linux-amd64 +chmod +x act_runner-linux-amd64 +./act_runner-linux-amd64 register --instance http://192.168.86.11:3005 --token +./act_runner-linux-amd64 daemon +``` + +Workflows at `.gitea/workflows/`: +- `ci.yml` — Rust tests + clippy + fmt + UI build (every push) +- `benchmark.yml` — Proxy latency P99 gate (pushes to src/proxy/) +- `deploy.yml` — Fly.io + Cloudflare Pages deploy (main branch) + +## Secrets Required + +Set in Gitea → Repository → Settings → Secrets: +- `FLY_API_TOKEN` — Fly.io deploy token +- `CLOUDFLARE_API_TOKEN` — Cloudflare Pages deploy +- `CLOUDFLARE_ACCOUNT_ID` — Cloudflare account +- `DATABASE_URL` — Neon connection string +- `REDIS_URL` — Upstash connection string +- `JWT_SECRET` — Production JWT signing key + +## DNS + +``` +route.dd0c.dev → Fly.io proxy (CNAME dd0c-route-proxy.fly.dev) +api.dd0c.dev → Fly.io API (CNAME dd0c-route-api.fly.dev) +app.dd0c.dev → Cloudflare Pages (auto) +```