Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

SELF_HOST.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.

SELF_HOST.mdBlame181 lines · 2 contributors
f2c00b4CC LABS App1# Gluecron self-host — eating our own dog food
2
5472beaccanty labs3> **Status (2026-07-09): the mirror is being CUT.** Gluecron is now
4> self-canonical — no GitHub mirror, no fallback. This doc is the background
5> design; the live cutover steps (offsite backup, freeze-then-delete the
6> GitHub repo, retire `hetzner-deploy.yml`) are in **`docs/CUTOVER_RUNBOOK.md`**.
7> The old "keep GitHub as a 7-day/30-day fallback" guidance below is superseded
8> by the runbook, which sequences durability (backups) before removal.
9
f2c00b4CC LABS App10> BLOCK W. The moment Gluecron's own source code stops living on GitHub
11> and starts living on Gluecron itself.
12
13## Why
14
15Today every push to `ccantynz-alt/Gluecron.com` on GitHub fires the
16`hetzner-deploy.yml` workflow, which SSHes into the box and runs a deploy
17script. Every red Action means a manual SSH deploy. Every AI safeguard
18(auto-merge, AI review, gate enforcement) only applies to **Gluecron-
19hosted repos** — so the platform's own source has been the one repo where
20none of the safety net runs.
21
22After this block lands and the operator runs the bootstrap, `git push`
23fires the deploy directly via Gluecron's own post-receive hook in ~25
24seconds. No GitHub Actions in the middle, no SSH retries, no missing AI
25safeguards.
26
27The pitch to customers writes itself: *"Gluecron deploys Gluecron with
28Gluecron."*
29
30## Pre-flight (do this BEFORE the bootstrap)
31
321. **You have site-admin access.** Confirm `/admin/ops` renders for your
33 account.
342. **You have root SSH to the production box.** The bootstrap must run on
35 the box where `/opt/gluecron` lives, because it writes to
36 `$GIT_REPOS_PATH` and to `/opt/gluecron/repos/<owner>/<repo>.git/hooks/`.
373. **`DATABASE_URL` points at the live Neon DB.** Source
38 `/etc/gluecron.env` before running the script: `set -a && source
39 /etc/gluecron.env && set +a`.
404. **`git` is on PATH and the box can reach `github.com`.** The bootstrap
41 clones the GitHub source over HTTPS before push-mirroring it into the
42 local bare repo.
435. **Disk: ~200 MB free** for the temp mirror clone + the bare repo
44 itself.
456. **No SSH key required from your laptop yet.** Gluecron's git is HTTPS-
46 only for now; cutover is `https://gluecron.com/<owner>/<repo>.git`.
47
48## 1. Run the bootstrap
49
50On the box, as root:
51
52```bash
53cd /opt/gluecron
54set -a; source /etc/gluecron.env; set +a
55bun run scripts/self-host-bootstrap.ts
56```
57
58Optional flags:
59
60```bash
61bun run scripts/self-host-bootstrap.ts \
62 --owner=ccantynz \
63 --name=Gluecron.com \
64 --source=https://github.com/ccantynz-alt/Gluecron.com.git \
65 --dry-run # print what would happen, change nothing
66```
67
68What happens (every step prints v/x/!):
69
701. Looks up the operator — first row in `site_admins`, falling back to
71 the oldest user (the bootstrap admin).
722. INSERTs `repositories(name='Gluecron.com', ownerId=<operator>,
73 isPrivate=false, defaultBranch='main', diskPath=<computed>)`. Skips
74 if a row already exists.
753. `git init --bare /opt/gluecron/repos/ccantynz/Gluecron.com.git` —
76 skips if the bare repo already exists.
774. `git clone --mirror` the GitHub source into a temp dir, then `git
78 push --mirror` into the bare repo. Every branch, tag, and commit is
79 transferred.
805. Writes `hooks/post-receive` on the bare repo: a one-line bash script
81 that invokes `scripts/self-deploy.sh` when ref is `refs/heads/main`.
826. Prints cutover instructions.
83
84The script is idempotent — re-run anytime. It only fails if a step
85truly cannot proceed (no users in DB, mirror push rejected, etc.).
86
87## 2. Cutover
88
89### On your laptop
90
91```bash
92cd ~/code/Gluecron.com
93git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git
94```
95
96### On the production box
97
98```bash
99cd /opt/gluecron
100git remote set-url origin https://gluecron.com/ccantynz/Gluecron.com.git
101```
102
103### Flip the self-host env var
104
105Add to `/etc/gluecron.env`:
106
107```
108SELF_HOST_REPO=ccantynz/Gluecron.com
109```
110
111Then reload systemd so the live process picks it up:
112
113```bash
114systemctl restart gluecron
115```
116
117## 3. Verify
118
1191. Open `/admin/self-host` in the browser. All three status pills should
120 be green: **Mirrored**, **Installed**, **Set**.
1212. From your laptop, push an empty commit:
122
123 ```bash
124 git commit --allow-empty -m "self-host smoke"
125 git push
126 ```
127
1283. Watch `/admin/deploys` — a new row appears with `source='self-deploy'`
129 and streams through `git-pull → bun-install → db-migrate → build →
130 restart-service → healthz → full-smoke`.
1314. Tail the log on the box: `tail -f /var/log/gluecron-self-deploy.log`.
132
133Total wall-clock: push to live ≈ 20–30 seconds.
134
135## 4. Rollback plan
136
137If a self-deploy breaks the site:
138
139- **Automatic:** `scripts/self-deploy.sh` runs `post-deploy-smoke.ts`
140 after every restart. On failure it `git reset --hard <PREV_SHA>` and
141 restarts. The previous-good SHA is captured *before* `git pull`, so
142 rollback is reflog-safe.
143- **Manual fallback to GitHub:** change the remote back and re-fire the
144 old workflow.
145
146 ```bash
147 # on laptop AND on the box
148 git remote set-url origin https://github.com/ccantynz-alt/Gluecron.com.git
149 ```
150
151 The GitHub mirror remains the canonical history for at least 30 days
152 after cutover — we don't deprecate `hetzner-deploy.yml` until the
153 self-host path has run a full week without intervention.
154
155## 5. Deprecating GitHub Actions
156
157After 7 days of green self-deploys:
158
1591. Set the workflow `on:` trigger to `workflow_dispatch:` only (no push
160 trigger) so it stays available for emergencies but doesn't fire on
161 every push.
1622. After 30 days, remove the `.github/workflows/hetzner-deploy.yml` file
163 entirely.
164
165Until then it's a free hot-spare deploy path.
166
167## Operator footnotes
168
169- **Log rotation:** `/var/log/gluecron-self-deploy.log` is appended to
170 forever. Add a `/etc/logrotate.d/gluecron-self-deploy` entry — TODO.
171- **Permissions:** `scripts/self-deploy.sh` runs as root (it has to,
172 because of `systemctl restart`). The bare-repo post-receive hook
173 inherits whatever user the HTTP receive-pack runs under (usually root
174 in our setup); SSH receive-pack will eventually need a `git` user with
175 passwordless `sudo systemctl restart gluecron`.
176- **Self-deploy is gated by `SELF_HOST_REPO`.** Customer repos named
177 `Gluecron.com` are safe — only the exact `<owner>/<repo>` slug in the
178 env var fires the local deploy.
179- **The optional `.gluecron/workflows/deploy.yml`** runs `self-deploy.sh
180 --inline` on the in-process workflow runner if you want belt-and-
181 braces. The post-receive hook is the primary path.