CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
DEPLOYMENT_RUNBOOK.md
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 7703650 | 1 | # Gluecron Deployment Runbook |
| 2 | ||
| 3 | Step-by-step guide for taking Gluecron from zero to running at | |
| 4 | `gluecron.com`. This runbook targets **infrastructure deployment** — | |
| 5 | Gluecron is deployed as free internal infrastructure for Craig's | |
| 6 | ecosystem, **not** as a paid product. | |
| 7 | ||
| 9dd96b9 | 8 | > **Day-to-day operations** (enable AI auto-merge, trigger a deploy, |
| 9 | > rollback) live at [`/admin/ops`](https://gluecron.com/admin/ops) once | |
| 10 | > the box is up. Deploy progress streams to | |
| 11 | > [`/admin/deploys`](https://gluecron.com/admin/deploys). This runbook | |
| 12 | > covers **first-time bootstrap only** — terminal steps below are | |
| 13 | > appropriate because there is no service yet to click against. | |
| 14 | ||
| 7703650 | 15 | --- |
| 16 | ||
| 17 | ## What Gluecron is today | |
| 18 | ||
| 19 | - Git hosting (clone / push / fetch over Smart HTTP) | |
| 20 | - Web UI: issues, PRs, code browser, webhooks, SSH keys, OAuth, 2FA | |
| 21 | - Integrates with **GateTest** (scans on push) and **Crontech** | |
| 22 | (deploys on push to `main`) | |
| 23 | - **Not a paid product.** No Stripe. No tiers. Free infrastructure | |
| 24 | for Craig's ecosystem. | |
| 25 | - Pre-launch banner visible; public signups gated until owner review. | |
| 26 | ||
| 27 | --- | |
| 28 | ||
| 29 | ## Prerequisites | |
| 30 | ||
| 31 | - **Neon** account (managed Postgres) — <https://neon.tech> | |
| 32 | - **Fly.io** *or* **Railway** account (container deployment) | |
| 33 | - Domain `gluecron.com` in hand (registrar access for DNS records) | |
| 34 | - *(Optional)* **Resend** account for transactional email | |
| 35 | - *(Optional)* **Anthropic** API key for AI features (copilot, | |
| 36 | changelog summarisation, semantic search) | |
| 37 | - *(Optional)* **Voyage** API key for semantic code search embeddings | |
| 38 | - Local tooling: `bun`, `git`, `flyctl` or `railway` CLI | |
| 39 | ||
| 40 | --- | |
| 41 | ||
| 42 | ## Phase 1 — Provision Postgres (Neon) | |
| 43 | ||
| 44 | 1. Sign in to <https://neon.tech> and create a project named | |
| 45 | `gluecron`. | |
| 46 | 2. From the project dashboard, copy the pooled connection string. | |
| 47 | It looks like: | |
| 7a03066 | 48 | `postgresql://user:REDACTED@ep-xxx.eu-west-2.aws.neon.tech/neondb?sslmode=require` |
| 7703650 | 49 | 3. Save this value — it becomes `DATABASE_URL` in Phase 3. |
| 50 | 4. **Do NOT run migrations yet.** The container runs them as its | |
| 51 | release step (see `fly.toml` → `[deploy] release_command`). | |
| 52 | ||
| 53 | --- | |
| 54 | ||
| 55 | ## Phase 2 — Choose a platform | |
| 56 | ||
| 57 | Both platforms ship from the checked-in `Dockerfile`. Pick one. | |
| 58 | ||
| 59 | ### Option A — Fly.io (recommended; `fly.toml` already exists) | |
| 60 | ||
| 61 | `fly.toml` already configures the persistent `gluecron_repos` | |
| 62 | volume mounted at `/app/repos` and wires up migrations as the | |
| 63 | release command. | |
| 64 | ||
| 65 | ```bash | |
| 66 | fly auth login | |
| 67 | fly launch --no-deploy # accept existing fly.toml | |
| 68 | fly volumes create gluecron_repos --size 10 --region lhr | |
| 69 | fly secrets set \ | |
| b105d39 | 70 | DATABASE_URL="<DATABASE_URL_FROM_NEON>" \ |
| 7703650 | 71 | APP_BASE_URL="https://gluecron.com" \ |
| 72 | WEBAUTHN_RP_ID="gluecron.com" \ | |
| 73 | WEBAUTHN_ORIGIN="https://gluecron.com" \ | |
| 74 | WEBAUTHN_RP_NAME="gluecron" \ | |
| 75 | GATETEST_URL="https://gatetest.io/api/scan/run" \ | |
| 76 | GATETEST_API_KEY="..." \ | |
| 77 | GATETEST_CALLBACK_SECRET="..." \ | |
| 78 | GATETEST_HMAC_SECRET="..." \ | |
| 79 | CRONTECH_DEPLOY_URL="https://crontech.ai/api/hooks/gluecron/push" \ | |
| 80 | GLUECRON_WEBHOOK_SECRET="..." \ | |
| 81 | CRONTECH_EVENT_TOKEN="..." \ | |
| 82 | ANTHROPIC_API_KEY="..." \ | |
| 83 | EMAIL_PROVIDER="resend" \ | |
| 84 | RESEND_API_KEY="..." \ | |
| 85 | EMAIL_FROM="gluecron <no-reply@gluecron.com>" | |
| 86 | fly deploy | |
| 87 | ``` | |
| 88 | ||
| 89 | ### Option B — Railway (`railway.toml` already exists) | |
| 90 | ||
| 91 | 1. <https://railway.app> → new project → **Deploy from GitHub**. | |
| 92 | 2. Select the Gluecron repo; Railway picks up the `Dockerfile` | |
| 93 | via `railway.toml`. | |
| 94 | 3. Add a persistent volume mounted at `/app/repos` (Railway UI | |
| 95 | → service → Volumes). | |
| 96 | 4. Set all environment variables from Phase 3 in the Railway | |
| 97 | dashboard. | |
| 98 | 5. Railway will run `bun run db:migrate` as the release command | |
| 99 | on deploy. | |
| 100 | ||
| 101 | --- | |
| 102 | ||
| 103 | ## Phase 3 — Environment variables | |
| 104 | ||
| 105 | All variables that Gluecron reads from `process.env`, enumerated | |
| 106 | from `src/lib/config.ts` and direct `process.env.*` references in | |
| 107 | `src/`. | |
| 108 | ||
| 109 | ### Core runtime | |
| 110 | ||
| 111 | | Variable | Required? | Example | Notes | | |
| 112 | |---|---|---|---| | |
| b105d39 | 113 | | `DATABASE_URL` | **Yes** | `<DATABASE_URL_FROM_NEON>` | Neon pooled connection string. | |
| 7703650 | 114 | | `PORT` | No | `3000` | Preset in `fly.toml` / `railway.toml`. | |
| 115 | | `GIT_REPOS_PATH` | **Yes** in prod | `/app/repos` | Must be a persistent volume — this is where bare repos live. | | |
| 116 | | `NODE_ENV` | **Yes** in prod | `production` | Enables secure cookies and rate limiting. Preset in both configs. | | |
| 117 | | `APP_BASE_URL` | **Yes** | `https://gluecron.com` | Canonical base URL; used in outbound webhooks and email links. No trailing slash. | | |
| 118 | ||
| 119 | ### WebAuthn (passkeys / 2FA) | |
| 120 | ||
| 121 | | Variable | Required? | Example | Notes | | |
| 122 | |---|---|---|---| | |
| 123 | | `WEBAUTHN_RP_ID` | **Yes** | `gluecron.com` | Domain only, **no scheme, no port**. Passkeys are bound to this value; changing it invalidates existing keys. | | |
| 124 | | `WEBAUTHN_ORIGIN` | **Yes** | `https://gluecron.com` | Full origin including scheme. Must match what the browser sees. | | |
| 125 | | `WEBAUTHN_RP_NAME` | No | `gluecron` | Human-facing name shown by the browser. Default `gluecron`. | | |
| 126 | ||
| 127 | ### GateTest integration (scans on push) | |
| 128 | ||
| 129 | | Variable | Required? | Example | Notes | | |
| 130 | |---|---|---|---| | |
| a4e1564 | 131 | | `GATETEST_URL` | **Yes** (for integration) | `https://gatetest.ai/api/events/push` | Default `https://gatetest.ai/api/events/push`. | |
| 7703650 | 132 | | `GATETEST_API_KEY` | **Yes** | `gtk_...` | Outbound bearer token sent to GateTest. | |
| 133 | | `GATETEST_CALLBACK_SECRET` | **Yes** | *(random 32-byte hex)* | Bearer token GateTest uses when posting scan results back to Gluecron (`/hooks/gatetest`). | | |
| 134 | | `GATETEST_HMAC_SECRET` | **Yes** | *(random 32-byte hex)* | HMAC signing secret for GateTest → Gluecron callbacks. | | |
| 135 | ||
| 136 | ### Crontech integration (deploys on push to main) | |
| 137 | ||
| 138 | | Variable | Required? | Example | Notes | | |
| 139 | |---|---|---|---| | |
| 140 | | `CRONTECH_DEPLOY_URL` | **Yes** (for integration) | `https://crontech.ai/api/hooks/gluecron/push` | Outbound webhook target. | | |
| 141 | | `GLUECRON_WEBHOOK_SECRET` | **Yes** | *(random 32-byte hex)* | Bearer token on outbound Crontech deploy webhook. Empty → Crontech returns 401 and deploys fail. | | |
| 142 | | `CRONTECH_EVENT_TOKEN` | **Yes** | *(random 32-byte hex)* | Bearer token Crontech uses when posting deploy events back to Gluecron (`/api/deploy-events`). | | |
| 143 | ||
| 144 | ### Email (optional — `log` provider works without) | |
| 145 | ||
| 146 | | Variable | Required? | Example | Notes | | |
| 147 | |---|---|---|---| | |
| 148 | | `EMAIL_PROVIDER` | No | `resend` or `log` | Default `log` (writes to stderr). Set `resend` for real sending. | | |
| 149 | | `RESEND_API_KEY` | Only if `EMAIL_PROVIDER=resend` | `re_...` | Resend API key. | | |
| 150 | | `EMAIL_FROM` | No | `gluecron <no-reply@gluecron.com>` | From header. Default `gluecron <no-reply@gluecron.local>`. | | |
| 151 | ||
| 152 | ### AI / semantic search (optional) | |
| 153 | ||
| 154 | | Variable | Required? | Example | Notes | | |
| 155 | |---|---|---|---| | |
| 156 | | `ANTHROPIC_API_KEY` | No | `sk-ant-...` | Enables copilot, PR review, changelog summarisation. | | |
| 157 | | `VOYAGE_API_KEY` | No | `pa-...` | Enables Voyage embeddings for semantic code search. Falls back to a local model when absent. | | |
| 158 | ||
| 159 | **Total: 18 required/useful env vars** (plus `NODE_ENV`, `PORT`, | |
| 160 | `VOYAGE_API_KEY` as optional tuning). | |
| 161 | ||
| 162 | --- | |
| 163 | ||
| 164 | ## Phase 4 — Run migrations | |
| 165 | ||
| 166 | On both Fly.io and Railway, `bun run db:migrate` is the release | |
| 167 | command and runs automatically before traffic is routed to a new | |
| 9dd96b9 | 168 | revision. Once the box is up, the easiest way to re-run migrations is |
| 169 | to click **"Trigger a deploy"** on | |
| 170 | [`/admin/ops`](https://gluecron.com/admin/ops) — the release command | |
| 171 | fires `bun run db:migrate` as part of the redeploy. | |
| 172 | ||
| 173 | Verify the server starts cleanly — `bun run` should not error on | |
| 174 | boot. | |
| 175 | ||
| 176 | <details> | |
| 177 | <summary>Manual fallback (terminal)</summary> | |
| 178 | ||
| 179 | Only needed during first-time bootstrap before `/admin/ops` is | |
| 180 | reachable, or if `/admin/ops` itself is broken. | |
| 7703650 | 181 | |
| 182 | ```bash | |
| 183 | # Fly.io | |
| 184 | fly ssh console | |
| 185 | bun run db:migrate | |
| 186 | ||
| 187 | # Railway | |
| 188 | railway run bun run db:migrate | |
| 189 | ``` | |
| 190 | ||
| 9dd96b9 | 191 | </details> |
| 7703650 | 192 | |
| 193 | --- | |
| 194 | ||
| 195 | ## Phase 5 — DNS | |
| 196 | ||
| 197 | 1. Point `gluecron.com` at your platform: | |
| 198 | - **Fly.io**: `fly ips list` → create `A` and `AAAA` records | |
| 199 | to the IPv4/IPv6 shown. Alternatively | |
| 200 | `fly certs add gluecron.com` and follow the CNAME/ALIAS | |
| 201 | instructions. | |
| 202 | - **Railway**: add `gluecron.com` in the service → Settings → | |
| 203 | Domains. Railway prints the target `CNAME`. | |
| 204 | 2. Wait for Let's Encrypt to provision the certificate (usually | |
| 205 | under 5 minutes). Verify: | |
| 206 | ||
| 207 | ```bash | |
| 208 | curl -I https://gluecron.com/ | |
| 209 | ``` | |
| 210 | ||
| 211 | --- | |
| 212 | ||
| 213 | ## Phase 6 — Smoke test | |
| 214 | ||
| 215 | - [ ] Visit <https://gluecron.com/> — pre-launch banner is visible. | |
| 216 | - [ ] Register an account via email / OAuth. | |
| 217 | - [ ] Log in, visit **Settings → SSH keys**, add an SSH public key. | |
| 218 | - [ ] Create a repository from the UI. | |
| 219 | - [ ] `git clone https://gluecron.com/<you>/<repo>.git` — must | |
| 220 | succeed (this was the Smart HTTP bug fixed in commit | |
| 221 | `676be75`). | |
| 222 | - [ ] `git push` — verify it triggers a GateTest scan (if | |
| 223 | `GATETEST_URL` is set). | |
| 224 | - [ ] `git push` to `main` — verify it triggers a Crontech deploy | |
| 225 | (if `CRONTECH_DEPLOY_URL` is set). | |
| 226 | ||
| 227 | --- | |
| 228 | ||
| 229 | ## Phase 7 — Integration wires with GateTest & Crontech | |
| 230 | ||
| 231 | The three apps share secrets pairwise. Each secret value **must be | |
| 232 | identical on both sides of the wire**. | |
| 233 | ||
| 234 | | Secret | Set on Gluecron | Set on the other side | | |
| 235 | |---|---|---| | |
| 236 | | `GATETEST_API_KEY` | outbound auth to GateTest | GateTest validates this on its `/api/scan/run` endpoint | | |
| 237 | | `GATETEST_CALLBACK_SECRET` | validates callbacks from GateTest | GateTest sends as bearer on scan-result POSTs | | |
| 238 | | `GATETEST_HMAC_SECRET` | verifies signatures on GateTest callbacks | GateTest signs outbound callbacks with this | | |
| 239 | | `GLUECRON_WEBHOOK_SECRET` | outbound auth to Crontech | Crontech validates this on `/api/hooks/gluecron/push` | | |
| 240 | | `CRONTECH_EVENT_TOKEN` | validates deploy-event POSTs from Crontech | Crontech sends as bearer on `/api/deploy-events` | | |
| 241 | ||
| 242 | **Rule of thumb:** generate each secret once with | |
| 243 | `openssl rand -hex 32`, then copy the same value into the matching | |
| 244 | variable on both services. | |
| 245 | ||
| 246 | --- | |
| 247 | ||
| 248 | ## Phase 8 — Pre-launch → launch | |
| 249 | ||
| 250 | When Craig is ready to open signups: | |
| 251 | ||
| 252 | 1. Remove the pre-launch banner — revert the `.prelaunch-banner` | |
| 253 | block in `src/views/layout.tsx` that was added in commit | |
| 254 | `4a52a98`. | |
| 255 | 2. Commit: `feat(launch): remove pre-launch banner`. | |
| 256 | 3. Push — the container redeploys automatically. | |
| 257 | ||
| 258 | --- | |
| 259 | ||
| 260 | ## Known limitations at launch | |
| 261 | ||
| 262 | - **No legal pages** — see `docs/legal-audit.md`; needs an owner | |
| 263 | decision on ToS / Privacy before broad public signups. | |
| 264 | - **No billing** — free forever, or bolt on Stripe later. | |
| 265 | - **18 remaining `tsc` errors in locked files** — they don't block | |
| 266 | runtime; clean-up tracked separately. | |
| 267 | ||
| 268 | --- | |
| 269 | ||
| 270 | ## Troubleshooting | |
| 271 | ||
| 272 | | Symptom | Likely cause | Fix | | |
| 273 | |---|---|---| | |
| 274 | | `git clone` reports "empty repo" | The repo has no commits. | Push at least one commit first — an empty bare repo has no refs to advertise. | | |
| 275 | | Smart HTTP returns `404` on `info/refs` | `GIT_REPOS_PATH` is wrong or the volume isn't mounted with write access. | Check the volume mount in `fly.toml` / Railway; confirm the container user can read the bare repo dir. | | |
| 276 | | WebAuthn / passkey registration fails | `WEBAUTHN_RP_ID` doesn't match the domain, or includes a scheme/port. | Must be the bare host (`gluecron.com`); ensure `WEBAUTHN_ORIGIN` matches what the browser sees, including `https://`. | | |
| 277 | | GateTest callbacks rejected with `401` | `GATETEST_CALLBACK_SECRET` mismatch between Gluecron and GateTest. | Re-generate the secret and set the same value on both services. | | |
| 278 | | Crontech deploys fail with `401` | `GLUECRON_WEBHOOK_SECRET` is empty or mismatched. | Set to the same value on both services; the config default is empty, which Crontech rejects. | | |
| 279 | | Outbound emails don't arrive | `EMAIL_PROVIDER` defaults to `log`. | Set `EMAIL_PROVIDER=resend` and configure `RESEND_API_KEY` + `EMAIL_FROM`. | | |
| 280 | ||
| 281 | --- | |
| 282 | ||
| b105d39 | 283 | *Last reviewed: 2026-04-16.* |