@@ -0,0 +1,616 @@ |
| 1 | +# shithub.sh — first-deploy setup guide |
| 2 | + |
| 3 | +This is the operator's running-order for taking shithub.sh from |
| 4 | +"Namecheap registration" to "live and serving signups." Walk it |
| 5 | +top-to-bottom, one step at a time. Each step has a verify-it- |
| 6 | +worked check. **Don't skip the verifications** — they're cheap |
| 7 | +and catch the wrong thing before it compounds. |
| 8 | + |
| 9 | +> **Time budget.** Total ~5–8 hours across 1–2 days, dominated |
| 10 | +> by DNS / Postmark verification waits (you can step away during |
| 11 | +> those). |
| 12 | + |
| 13 | +> **Money budget.** ~$3.50/day from Step 4 onwards |
| 14 | +> (~$105/mo all-in once Spaces buckets are provisioned). If you |
| 15 | +> have to pause for a day, fine; if you need to pause for a week, |
| 16 | +> destroy the droplets — the volume + Spaces + DNS persist. |
| 17 | + |
| 18 | +## Decisions baked in |
| 19 | + |
| 20 | +- Domain: **shithub.sh** (you registered this on Namecheap) |
| 21 | +- Region: **NYC3** (DO Spaces parity, codebase default) |
| 22 | +- DR region: **SFO3** (cross-region Spaces mirror) |
| 23 | +- Email: **Postmark** (free tier covers v0.1.0) |
| 24 | +- Mirror plan: **90-day GitHub mirror, then drop** |
| 25 | +- Version: **v0.1.0** (pre-1.0; honest about WIP; tag will be cut later) |
| 26 | +- On-call: **email-only alerts for week 1**, flip to phone after |
| 27 | + noise calibration |
| 28 | + |
| 29 | +If any of these change, redo the steps that depend on them. |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Phase A — Accounts & DNS (do these first; they propagate while you wait) |
| 34 | + |
| 35 | +### A1. Postmark account |
| 36 | + |
| 37 | +1. Go to <https://account.postmarkapp.com/sign_up>. |
| 38 | +2. Sign up with the email you'll use for ops |
| 39 | + (`ops@shithub.sh` is a good convention; you'll set up the |
| 40 | + inbox later). |
| 41 | +3. After signup, Postmark drops you in a default Server. |
| 42 | + Rename it: top-left dropdown → **Manage Servers** → click the |
| 43 | + default → **Settings** → name it **shithub-prod**. |
| 44 | +4. Confirm the Server has the **Transactional** message stream |
| 45 | + (default) — it does for new accounts. |
| 46 | +5. **Don't grab the API token yet** — we generate it after |
| 47 | + verifying the domain. |
| 48 | + |
| 49 | +**Verify:** the Servers list shows one server named |
| 50 | +`shithub-prod`. |
| 51 | + |
| 52 | +### A2. Verify the sender domain in Postmark |
| 53 | + |
| 54 | +This is the slow step. Start it now; come back later. |
| 55 | + |
| 56 | +1. In the Postmark dashboard: top-right **Sender Signatures** → |
| 57 | + **Domains** tab → **Add Domain**. |
| 58 | +2. Enter **`shithub.sh`** (the bare apex — DKIM applies to all |
| 59 | + subdomains). |
| 60 | +3. Postmark presents three DNS records to add: |
| 61 | + - **DKIM** — a `TXT` record at `<postmark-prefix>._domainkey` |
| 62 | + - **Return-Path** — a `CNAME` at `pm-bounces` (for VERP-style |
| 63 | + bounce handling) |
| 64 | + - **(optional) DMARC** — a `TXT` at `_dmarc` — Postmark will |
| 65 | + suggest one; we'll add it. |
| 66 | +4. Leave that tab open; we add the records in Namecheap next. |
| 67 | + |
| 68 | +### A3. Add Postmark DNS records in Namecheap |
| 69 | + |
| 70 | +1. <https://ap.www.namecheap.com/Domains/DomainControlPanel/shithub.sh/advancedns> |
| 71 | +2. **Advanced DNS** tab. Set **NameServers** to **Namecheap |
| 72 | + BasicDNS** if it isn't already (the default after a fresh |
| 73 | + registration). |
| 74 | +3. **Add a New Record** for each of the three Postmark records. |
| 75 | + Match the exact `Host` and `Value` Postmark gave you. TTL = |
| 76 | + 1 min during setup (we'll relax it later). |
| 77 | +4. Also add an **SPF record** if there isn't one already: |
| 78 | + - Type: `TXT` |
| 79 | + - Host: `@` |
| 80 | + - Value: `v=spf1 include:spf.mtasv.net ~all` |
| 81 | + - (`spf.mtasv.net` is Postmark's relay; the `~all` is |
| 82 | + soft-fail.) |
| 83 | +5. **DMARC record** (recommended; Postmark prompts you): |
| 84 | + - Type: `TXT` |
| 85 | + - Host: `_dmarc` |
| 86 | + - Value: `v=DMARC1; p=none; rua=mailto:dmarc-rua@shithub.sh; pct=100` |
| 87 | + - `p=none` means "report only, don't reject" — appropriate |
| 88 | + for week 1 while we tune. Tighten to `p=quarantine` later. |
| 89 | + |
| 90 | +**Verify:** in Postmark's **Domains** tab, click **Verify**. |
| 91 | +DKIM may take 5–30 min to propagate. Other records refresh on |
| 92 | +their own. The verification turns green once all records are |
| 93 | +seen. Move on; come back to confirm. |
| 94 | + |
| 95 | +### A4. Generate Postmark API token |
| 96 | + |
| 97 | +After domain shows verified: |
| 98 | + |
| 99 | +1. Postmark → **Servers** → **shithub-prod** → **API Tokens** |
| 100 | + tab. |
| 101 | +2. The default Server token shown there is what we'll use. |
| 102 | +3. **Copy it.** Keep in your password manager. |
| 103 | +4. **Sender from:** decide on the From address. Convention: |
| 104 | + `shithub <noreply@shithub.sh>`. Postmark will accept any |
| 105 | + address on the verified domain; no per-address signature |
| 106 | + needed. |
| 107 | + |
| 108 | +### A5. Set up DNS for the app + docs subdomains |
| 109 | + |
| 110 | +Still in Namecheap **Advanced DNS** for shithub.sh. Add: |
| 111 | + |
| 112 | +- `A` record, Host `@`, Value `<APP-DROPLET-IP>` — **placeholder |
| 113 | + for now**; we'll fill the real IP after creating the droplet |
| 114 | + in Phase B. **Pin TTL to 1 min** for now. |
| 115 | +- `A` record, Host `www`, Value `<APP-DROPLET-IP>` — same. |
| 116 | +- `CNAME` record, Host `docs`, Value `shithub-docs.nyc3.cdn.digitaloceanspaces.com.` |
| 117 | + (Spaces CDN — we'll create the bucket in Phase B; the CNAME |
| 118 | + resolves once the bucket exists.) The trailing dot matters. |
| 119 | + |
| 120 | +Skip the records you don't have IPs for yet; they go in after |
| 121 | +Phase B step B3. |
| 122 | + |
| 123 | +### A6. Telegram bot for alerts (skipped — week-1 email only) |
| 124 | + |
| 125 | +We're using email-only alerts for week 1. Email goes via |
| 126 | +Postmark to your ops mailbox. When you flip to phone alerts: |
| 127 | +follow `docs/internal/runbooks/incidents.md` and add a Telegram |
| 128 | +bot — won't repeat that here. |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Phase B — DigitalOcean infrastructure |
| 133 | + |
| 134 | +### B1. DO project + SSH key |
| 135 | + |
| 136 | +1. <https://cloud.digitalocean.com> → **Projects** (left sidebar) |
| 137 | + → **New Project**. |
| 138 | +2. Name: **shithub-prod**. Purpose: **Service or API**. |
| 139 | + Environment: **Production**. |
| 140 | +3. **SSH key:** Account (top-right) → Settings → Security → |
| 141 | + **Add SSH Key**. Paste the contents of |
| 142 | + `~/.ssh/id_ed25519.pub` from your laptop. Name it after the |
| 143 | + laptop ("macbook-pro"). |
| 144 | + |
| 145 | +**Verify:** the project shows in the dropdown; the SSH key |
| 146 | +shows under Settings → Security. |
| 147 | + |
| 148 | +### B2. Create Spaces buckets (do these BEFORE droplets — they need to exist for the docs CNAME to resolve) |
| 149 | + |
| 150 | +1. Left sidebar → **Spaces Object Storage** → **Create Spaces |
| 151 | + Bucket**. |
| 152 | +2. **Bucket #1 — primary backups + docs:** |
| 153 | + - Region: **NYC3** |
| 154 | + - Name: **shithub-prod** (this matches the inventory's |
| 155 | + `s3_bucket: shithub-prod`) |
| 156 | + - File listing: **Restrict** (private; presigned URLs only) |
| 157 | + - CDN: **Enable** (for the docs subdomain) |
| 158 | + - Project: **shithub-prod** |
| 159 | +3. **Bucket #2 — DR mirror:** |
| 160 | + - Region: **SFO3** |
| 161 | + - Name: **shithub-prod-dr** |
| 162 | + - Same other settings. |
| 163 | +4. **Bucket #3 — docs site:** |
| 164 | + - Region: **NYC3** |
| 165 | + - Name: **shithub-docs** |
| 166 | + - File listing: **Public** (it's the docs site; everyone |
| 167 | + reads) |
| 168 | + - CDN: **Enable** |
| 169 | + - Project: **shithub-prod** |
| 170 | +5. **Generate Spaces access keys:** Account → API → **Spaces |
| 171 | + Keys** → **Generate New Key**. Name it `shithub-prod-app`. |
| 172 | + Copy the access key + secret — Postmark-style, the secret |
| 173 | + is shown once. |
| 174 | + |
| 175 | +**Verify:** three buckets listed in Spaces, all in their |
| 176 | +respective regions. Endpoint URLs follow the pattern |
| 177 | +`<bucket>.<region>.digitaloceanspaces.com`. |
| 178 | + |
| 179 | +### B3. Create the four droplets |
| 180 | + |
| 181 | +Use the DO web UI for the first one to confirm the shape; then |
| 182 | +duplicate. |
| 183 | + |
| 184 | +1. Left sidebar → **Droplets** → **Create Droplet**. |
| 185 | +2. **Region:** NYC3 → Datacenter NYC3. |
| 186 | +3. **Image:** Marketplace? **No** — Distributions tab → Ubuntu |
| 187 | + 24.04 (LTS) x64. |
| 188 | +4. **Size:** Basic → Regular SSD → **2 vCPU / 4 GB RAM / |
| 189 | + 80 GB SSD** ($24/mo). |
| 190 | +5. **VPC Network:** default VPC for NYC3 (DO selects this |
| 191 | + automatically). All four droplets must be in the same VPC so |
| 192 | + they see each other on private IPs. |
| 193 | +6. **Authentication:** SSH Key → check the key you added in B1. |
| 194 | +7. **Hostname:** `shithub-app` (this is droplet #1). |
| 195 | +8. **Tags:** `shithub`, `shithub-app`. |
| 196 | +9. **Project:** shithub-prod. |
| 197 | +10. **Backups:** off (we have our own backup pipeline). |
| 198 | +11. **Monitoring:** **on** (DO's free agent — useful baseline |
| 199 | + metrics in their UI). |
| 200 | +12. Create. |
| 201 | + |
| 202 | +Repeat for droplets #2–#4 with these differences: |
| 203 | + |
| 204 | +| # | Hostname | Size | Tags | |
| 205 | +|---|--------------------|----------------------|----------------------------| |
| 206 | +| 2 | `shithub-db` | s-2vcpu-4gb ($24/mo) | `shithub`, `shithub-db` | |
| 207 | +| 3 | `shithub-backup` | s-1vcpu-2gb ($12/mo) | `shithub`, `shithub-backup`| |
| 208 | +| 4 | `shithub-monitoring`| s-2vcpu-4gb ($24/mo)| `shithub`, `shithub-monitoring` | |
| 209 | + |
| 210 | +**Capture the IPs.** For each droplet, write down both the |
| 211 | +**public IPv4** (for SSH from your laptop, and for `shithub-app` |
| 212 | +to point DNS at) and the **private IPv4** (for inter-droplet |
| 213 | +traffic — appears under "Private IPv4" in the droplet detail). |
| 214 | + |
| 215 | +**Now go back to Phase A5** and update the `A` records for `@` |
| 216 | +and `www` to point at `shithub-app`'s public IPv4. The CNAME |
| 217 | +for `docs` you set already; it will start resolving once |
| 218 | +shithub-docs CDN is ready (~1 min). |
| 219 | + |
| 220 | +### B4. Create + attach the block volume |
| 221 | + |
| 222 | +1. Left sidebar → **Volumes** → **Create Volume**. |
| 223 | +2. **Region:** NYC3. |
| 224 | +3. **Size:** **100 GB** ($10/mo). |
| 225 | +4. **Filesystem format:** Ext4. **Mount options:** automatic. |
| 226 | +5. **Attach to droplet:** `shithub-app`. |
| 227 | +6. **Mount point:** **`/mnt/shithub_data`** (DO's default for the |
| 228 | + volume name). |
| 229 | +7. Create + attach. |
| 230 | + |
| 231 | +**Verify (after SSHing in B5):** |
| 232 | +```sh |
| 233 | +df -h /mnt/shithub_data # should show ~100 GB Ext4 mounted |
| 234 | +``` |
| 235 | + |
| 236 | +We'll move the mount to `/data` (where the playbook expects it) |
| 237 | +during Phase C bind-mount step. |
| 238 | + |
| 239 | +### B5. SSH-bootstrap — confirm you can reach all four droplets |
| 240 | + |
| 241 | +From your laptop: |
| 242 | + |
| 243 | +```sh |
| 244 | +# Replace IPs with the public IPv4 of each droplet. |
| 245 | +ssh root@<APP-IP> # shithub-app |
| 246 | +ssh root@<DB-IP> # shithub-db |
| 247 | +ssh root@<BACKUP-IP> # shithub-backup |
| 248 | +ssh root@<MONITORING-IP> # shithub-monitoring |
| 249 | +``` |
| 250 | + |
| 251 | +If `Permission denied (publickey)`, your SSH key didn't get |
| 252 | +attached at create time; add it via DO console (Droplet → |
| 253 | +Access → Reset Root Password is the only fallback that works |
| 254 | +without an existing key). |
| 255 | + |
| 256 | +**Verify:** `whoami` returns `root` on each. |
| 257 | + |
| 258 | +### B6. Bootstrap inter-droplet SSH |
| 259 | + |
| 260 | +Ansible will use shithub-app as its control node (we install it |
| 261 | +there in Phase C). For Ansible to reach the other three over |
| 262 | +the private network, shithub-app needs an SSH key authorized on |
| 263 | +each. |
| 264 | + |
| 265 | +On **shithub-app**: |
| 266 | + |
| 267 | +```sh |
| 268 | +ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" |
| 269 | +cat /root/.ssh/id_ed25519.pub |
| 270 | +``` |
| 271 | + |
| 272 | +Copy that public key. On **shithub-db**, **shithub-backup**, and |
| 273 | +**shithub-monitoring**: |
| 274 | + |
| 275 | +```sh |
| 276 | +mkdir -p /root/.ssh |
| 277 | +cat >> /root/.ssh/authorized_keys <<'EOF' |
| 278 | +<paste shithub-app's pubkey here> |
| 279 | +EOF |
| 280 | +chmod 600 /root/.ssh/authorized_keys |
| 281 | +``` |
| 282 | + |
| 283 | +**Verify (from shithub-app):** |
| 284 | +```sh |
| 285 | +ssh root@<DB-PRIVATE-IP> hostname # → shithub-db |
| 286 | +ssh root@<BACKUP-PRIVATE-IP> hostname # → shithub-backup |
| 287 | +ssh root@<MONITORING-PRIVATE-IP> hostname # → shithub-monitoring |
| 288 | +``` |
| 289 | + |
| 290 | +If asked about host key fingerprints, say yes. |
| 291 | + |
| 292 | +--- |
| 293 | + |
| 294 | +## Phase C — Hand Claude Code the keyboard |
| 295 | + |
| 296 | +### C1. Install Claude Code on shithub-app |
| 297 | + |
| 298 | +On **shithub-app**: |
| 299 | + |
| 300 | +```sh |
| 301 | +apt-get update |
| 302 | +apt-get install -y curl ca-certificates |
| 303 | +curl -fsSL https://claude.ai/install.sh | sh |
| 304 | +``` |
| 305 | + |
| 306 | +Then run `claude` and authenticate with the same Anthropic |
| 307 | +account you're using on your laptop (browser flow on your |
| 308 | +laptop, paste the code into the SSH terminal). |
| 309 | + |
| 310 | +### C2. Install build dependencies on shithub-app |
| 311 | + |
| 312 | +These are needed to build the `shithubd` binary on the droplet: |
| 313 | + |
| 314 | +```sh |
| 315 | +apt-get install -y \ |
| 316 | + git make build-essential \ |
| 317 | + ansible \ |
| 318 | + golang-go |
| 319 | +# Verify Go ≥ 1.22 (the project targets 1.26). |
| 320 | +go version |
| 321 | +``` |
| 322 | + |
| 323 | +If the apt Go is too old (Ubuntu 24.04 ships ~1.22 which is |
| 324 | +fine), skip the next part. If you need a newer Go, install via |
| 325 | +the tarball method: |
| 326 | + |
| 327 | +```sh |
| 328 | +curl -LO https://go.dev/dl/go1.22.5.linux-amd64.tar.gz |
| 329 | +rm -rf /usr/local/go |
| 330 | +tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz |
| 331 | +echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc |
| 332 | +source /root/.bashrc |
| 333 | +go version |
| 334 | +``` |
| 335 | + |
| 336 | +### C3. Clone the source |
| 337 | + |
| 338 | +```sh |
| 339 | +mkdir -p /root/src && cd /root/src |
| 340 | +git clone https://github.com/tenseleyFlow/shithub.git |
| 341 | +cd shithub |
| 342 | +git log --oneline -3 # confirm latest commit |
| 343 | +``` |
| 344 | + |
| 345 | +### C4. Move the volume mount to /data |
| 346 | + |
| 347 | +The Ansible playbook expects `/data` as the data root. We could |
| 348 | +edit the inventory, but it's cleaner to bind-mount the |
| 349 | +DO-attached volume to `/data`: |
| 350 | + |
| 351 | +```sh |
| 352 | +mkdir -p /data |
| 353 | +mount --bind /mnt/shithub_data /data |
| 354 | +echo '/mnt/shithub_data /data none bind 0 0' >> /etc/fstab |
| 355 | + |
| 356 | +# Verify the bind mount survives the next reboot test: |
| 357 | +mount | grep -E '/(data|mnt/shithub_data)' |
| 358 | +``` |
| 359 | + |
| 360 | +### C5. Hand off to Claude Code |
| 361 | + |
| 362 | +From your SSH session on shithub-app, run: |
| 363 | + |
| 364 | +```sh |
| 365 | +cd /root/src/shithub |
| 366 | +claude |
| 367 | +``` |
| 368 | + |
| 369 | +When Claude is up, paste this priming message: |
| 370 | + |
| 371 | +> This is the shithub deploy you've been planning. The repo is at |
| 372 | +> github.com/tenseleyFlow/shithub. You wrote the sprint specs at |
| 373 | +> .docs/sprints/, especially S37-deploy.md and S40-launch.md. |
| 374 | +> We are at Phase D of `deploy/cutover/SETUP-GUIDE.md` — please |
| 375 | +> read that file and the prerequisite docs, then walk me through |
| 376 | +> Phase D with my hands on the keyboard. Domain is shithub.sh. |
| 377 | +> Postmark is set up. Spaces buckets are: shithub-prod (NYC3, |
| 378 | +> private), shithub-prod-dr (SFO3, private), shithub-docs (NYC3, |
| 379 | +> public). Volume bind-mounted at /data. Other droplets reachable |
| 380 | +> via private IPs. |
| 381 | + |
| 382 | +Claude will pick up from there. **The rest of this guide is |
| 383 | +written for Claude (or you, if you'd rather drive yourself).** |
| 384 | + |
| 385 | +--- |
| 386 | + |
| 387 | +## Phase D — Inventory + secrets |
| 388 | + |
| 389 | +### D1. Copy the production inventory template |
| 390 | + |
| 391 | +```sh |
| 392 | +cd /root/src/shithub |
| 393 | +cp deploy/ansible/inventory/production.example deploy/ansible/inventory/production |
| 394 | +``` |
| 395 | + |
| 396 | +The bare name `production` is gitignored so secrets stay out of |
| 397 | +the repo. |
| 398 | + |
| 399 | +### D2. Generate the cryptographic secrets |
| 400 | + |
| 401 | +```sh |
| 402 | +# Session signing key (cookie MAC). |
| 403 | +openssl rand -base64 32 > /tmp/session_key |
| 404 | +# TOTP AEAD key (encrypts 2FA secrets at rest). |
| 405 | +openssl rand -base64 32 > /tmp/totp_key |
| 406 | +# Postgres passwords. |
| 407 | +openssl rand -base64 24 > /tmp/db_password |
| 408 | +openssl rand -base64 24 > /tmp/hook_password |
| 409 | +# WireGuard private keys (one per host). |
| 410 | +for h in app db backup monitoring; do |
| 411 | + wg genkey > /tmp/wg_${h}.key |
| 412 | + wg pubkey < /tmp/wg_${h}.key > /tmp/wg_${h}.pub |
| 413 | +done |
| 414 | +``` |
| 415 | + |
| 416 | +### D3. Fill in the inventory |
| 417 | + |
| 418 | +```sh |
| 419 | +$EDITOR deploy/ansible/inventory/production |
| 420 | +``` |
| 421 | + |
| 422 | +Fill in: |
| 423 | +- `app_host`, `db_host`, `backup_host`, `monitoring_host` — the |
| 424 | + **private IPv4** of each droplet. |
| 425 | +- `domain: shithub.sh` |
| 426 | +- `caddy_email: ops@shithub.sh` (Let's Encrypt notifications) |
| 427 | +- `db_password`, `hook_password` — paste from `/tmp/`. |
| 428 | +- `session_key`, `totp_key` — paste from `/tmp/`. |
| 429 | +- `s3_endpoint: nyc3.digitaloceanspaces.com` |
| 430 | +- `s3_region: us-east-1` (Spaces uses this for SigV4) |
| 431 | +- `s3_bucket: shithub-prod` |
| 432 | +- `s3_access_key_id`, `s3_secret_access_key` — from B2 step 5. |
| 433 | +- `email_backend: postmark` |
| 434 | +- `postmark_server_token` — from A4. |
| 435 | +- `email_from: shithub <noreply@shithub.sh>` |
| 436 | +- `auth_base_url: https://shithub.sh` |
| 437 | +- WireGuard peer keys from `/tmp/wg_*.{key,pub}`. |
| 438 | + |
| 439 | +After filling in: |
| 440 | + |
| 441 | +```sh |
| 442 | +chmod 600 deploy/ansible/inventory/production |
| 443 | +shred -u /tmp/session_key /tmp/totp_key /tmp/db_password \ |
| 444 | + /tmp/hook_password /tmp/wg_*.key |
| 445 | +``` |
| 446 | + |
| 447 | +(Keep the public WireGuard keys in case you need to add a peer |
| 448 | +later; the private keys are now only in the inventory.) |
| 449 | + |
| 450 | +--- |
| 451 | + |
| 452 | +## Phase E — Deploy |
| 453 | + |
| 454 | +### E1. Dry-run |
| 455 | + |
| 456 | +```sh |
| 457 | +cd /root/src/shithub |
| 458 | +make deploy-check ANSIBLE_INVENTORY=production |
| 459 | +``` |
| 460 | + |
| 461 | +Read the diff. Expect every host to be `changed`. If any host |
| 462 | +shows `unreachable`, the SSH bootstrap from B6 missed a droplet. |
| 463 | + |
| 464 | +### E2. Build the binary with the version stamp |
| 465 | + |
| 466 | +We're not cutting the v0.1.0 tag yet — that's a launch-day |
| 467 | +ceremony. For this first deploy, the binary will stamp the |
| 468 | +short commit + build time, which is fine; the soft-launch |
| 469 | +window catches surprises before we tag. |
| 470 | + |
| 471 | +```sh |
| 472 | +make build |
| 473 | +./bin/shithubd version |
| 474 | +# expect: |
| 475 | +# Version: <short-commit-or-dev> |
| 476 | +# Commit: <short-commit> |
| 477 | +# Built: <today, UTC> |
| 478 | +``` |
| 479 | + |
| 480 | +### E3. Apply |
| 481 | + |
| 482 | +```sh |
| 483 | +make deploy ANSIBLE_INVENTORY=production |
| 484 | +``` |
| 485 | + |
| 486 | +Expect ~15–30 min on the first run. The roles run in this order: |
| 487 | +**base → postgres → shithubd → caddy → wireguard → backup → |
| 488 | +monitoring-client.** Caddy will obtain real Let's Encrypt certs |
| 489 | +on first request; `caddy_use_acme_staging` should be `false` in |
| 490 | +the inventory so we get a real cert. |
| 491 | + |
| 492 | +If a role fails, **stop**. Re-running with `--limit` and the |
| 493 | +specific role tag is the surgical path. Read the journal of the |
| 494 | +failing service before retrying: |
| 495 | +```sh |
| 496 | +journalctl -u <service> -n 200 |
| 497 | +``` |
| 498 | + |
| 499 | +### E4. Bootstrap the admin (you) |
| 500 | + |
| 501 | +```sh |
| 502 | +ssh root@<APP-PRIVATE-IP> |
| 503 | +sudo -u shithub /usr/local/bin/shithubd admin bootstrap-admin \ |
| 504 | + --email you@your-current-email.com |
| 505 | +``` |
| 506 | + |
| 507 | +The CLI prints a one-time password-reset link. Open it in a |
| 508 | +browser, set a password, **immediately enable 2FA**. |
| 509 | + |
| 510 | +### E5. Smoke |
| 511 | + |
| 512 | +```sh |
| 513 | +deploy/cutover/smoke.sh https://shithub.sh |
| 514 | +``` |
| 515 | + |
| 516 | +If everything is green, the soft launch is live. Signups are |
| 517 | +still gated (we set `SHITHUB_AUTH__SIGNUP_DISABLED=true` in the |
| 518 | +inventory by default for soft launch); you're the only user. |
| 519 | + |
| 520 | +--- |
| 521 | + |
| 522 | +## Phase F — Soft launch (24–48h) |
| 523 | + |
| 524 | +You're now using shithub yourself. Things to do: |
| 525 | + |
| 526 | +1. **Push the project's source.** Create the `shithub` org, |
| 527 | + create the `shithub` repo under it, push from this checkout. |
| 528 | +2. **Walk every flow.** Signup (toggle the gate, verify, gate |
| 529 | + again), password reset, 2FA, SSH key add (the SSH transport |
| 530 | + isn't shipped, so this just stores the key — fine), PAT |
| 531 | + create, repo create, push, issue, PR, review, merge, search. |
| 532 | +3. **Note every rough edge.** File issues against |
| 533 | + `shithub/shithub` itself. |
| 534 | +4. **Watch the dashboards.** Grafana on the monitoring host. |
| 535 | +5. **First daily backup runs at the next 03:00 UTC.** Confirm |
| 536 | + it landed in Spaces the morning after. |
| 537 | +6. **First restore drill.** When the second daily backup |
| 538 | + exists, run the drill. |
| 539 | + |
| 540 | +Day 2 morning: you have a real instance with real data + a real |
| 541 | +backup. You're as ready as you'll ever be. |
| 542 | + |
| 543 | +--- |
| 544 | + |
| 545 | +## Phase G — Public launch |
| 546 | + |
| 547 | +### G1. Tag v0.1.0 |
| 548 | + |
| 549 | +```sh |
| 550 | +cd /root/src/shithub |
| 551 | +git tag -a v0.1.0 -m "v0.1.0 — initial public release" |
| 552 | +git push origin v0.1.0 |
| 553 | +``` |
| 554 | + |
| 555 | +### G2. Build and re-deploy with the tagged version |
| 556 | + |
| 557 | +```sh |
| 558 | +git fetch --tags |
| 559 | +git checkout v0.1.0 |
| 560 | +make build |
| 561 | +./bin/shithubd version # Version: v0.1.0 |
| 562 | +make deploy ANSIBLE_INVENTORY=production |
| 563 | +``` |
| 564 | + |
| 565 | +Now the home page's Version field reads `v0.1.0`. Verify in a |
| 566 | +browser. |
| 567 | + |
| 568 | +### G3. Open signups |
| 569 | + |
| 570 | +Edit the inventory: |
| 571 | +```sh |
| 572 | +$EDITOR deploy/ansible/inventory/production |
| 573 | +# Change: signup_disabled: false |
| 574 | +make deploy ANSIBLE_INVENTORY=production ANSIBLE_TAGS=app |
| 575 | +``` |
| 576 | + |
| 577 | +### G4. Update the status page |
| 578 | + |
| 579 | +```sh |
| 580 | +$EDITOR docs/public/status.md |
| 581 | +# Update timestamp + "All systems normal." |
| 582 | +make docs # local mdBook build |
| 583 | +deploy/docs-site/sync-to-spaces.sh |
| 584 | +``` |
| 585 | + |
| 586 | +### G5. Post the announcement |
| 587 | + |
| 588 | +`docs/blog/v0.1.0-launch.md` is the copy. Submit to: |
| 589 | +- Hacker News |
| 590 | +- /r/programming, /r/selfhosted |
| 591 | +- lobste.rs |
| 592 | +- Mastodon |
| 593 | + |
| 594 | +You're live. Read `docs/internal/runbooks/day-one.md` next. |
| 595 | + |
| 596 | +--- |
| 597 | + |
| 598 | +## When something goes wrong |
| 599 | + |
| 600 | +- **Caddy won't get a cert.** DNS hasn't fully propagated, or |
| 601 | + port 80 is blocked. Check both. Last resort: flip |
| 602 | + `caddy_use_acme_staging=true`, redeploy, get the staging cert |
| 603 | + to confirm everything else works, then flip back when DNS is |
| 604 | + good. |
| 605 | +- **`/readyz` returns 503.** DB or storage unreachable. Check |
| 606 | + `journalctl -u shithubd-web` for the specific error. |
| 607 | +- **Postmark says emails go through but they don't arrive.** |
| 608 | + Check spam, then check Postmark's Activity tab — every send |
| 609 | + is logged. DKIM may not be propagated yet; first 24h is rough |
| 610 | + on cold-start deliverability. |
| 611 | +- **Ansible reports "host unreachable" mid-run.** SSH from |
| 612 | + shithub-app to the failing host's private IP; if that doesn't |
| 613 | + work, you're missing the key in B6. |
| 614 | + |
| 615 | +Anything else: read `docs/internal/runbooks/incidents.md` and |
| 616 | +the `troubleshooting.md` in self-host docs. |