Skip to content

dev2: fleet kit to move Railway sites onto dev2 and deploy them on merge - #103

Merged
ralyodio merged 49 commits into
masterfrom
ops/dev2-fleet-kit
Sep 25, 2026
Merged

ralyodio merged 49 commits into
masterfrom
ops/dev2-fleet-kit

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

dev2/dev2-site moves any Railway-hosted site onto dev2 in one resumable command and leaves the repo deploying there on merge: registry from Railway, scaffold PR (deploy-dev2.yml, nixpacks Dockerfile when needed), Postgres copy into the shared cluster, provisioning over ssh, certificate over Porkbun DNS-01 before DNS moves, nginx vhost, Porkbun flip, Railway stop + repo disconnect, merge, public verify. Second passes: per-site self-hosted Supabase (stack, pull, load, storage, cutover). Image-only services and companion services are supported through sites.d/.

63 of 79 sites went through it on 2026-09-25. dev2/README.md has the runbook and the traps.

🤖 Generated with Claude Code

ralyodio and others added 30 commits September 25, 2026 09:30
dev2-site reads the truth from Railway (services, domains, volumes, database
variables), renders the box files from templates, provisions ~/www/<site> over
ssh, copies Postgres through Railway's ssh endpoint into the shared cluster,
issues the certificate over Porkbun DNS-01 before DNS moves, flips Porkbun,
stops and disconnects the Railway service, and merges the per-repo
deploy-dev2.yml. First site through it: bg0ne.com.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…show deploy stderr

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… cutover) + proxied gateway hosts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With dozens of compose networks Docker moves to 192.168.x/20 pools, which an
allowlist of 172.16.0.0/12 alone drops (CONNECT_TIMEOUT to :5432 from the app).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…apps

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ify db url

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…heckouts

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… and node-postgres accept

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…austed)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uire otherwise)

Bun.SQL forwards unknown URL parameters to the server as GUCs, so
uselibpqcompat=true was FATAL there while the health check stayed green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ode 22

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ale embedded DNS)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o resolve

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…services only; opensocial.chat override

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… nodejs_18, --regen

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…PACKS_* command variables

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…clone/build); openaccess + openmcp overrides

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ralyodio and others added 6 commits September 25, 2026 10:50
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… commands

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate order

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread dev2/templates/supabase-pull.sh Fixed
Comment thread dev2/sites.d/tipoffwatch.com.json Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site
pw = cluster_pg_password()
# The app connects as `postgres` (the only role pg_hba lets in from another compose
# network); isolation is one database per app, same as nichedb and rssamplifier.
exists = pg_admin(f"select count(*) from pg_database where datname='{db}'").strip()
Comment thread dev2/sites.d/threatcrush.com.json Fixed
Comment thread dev2/sites.d/threatcrush.com.json Fixed
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

ThreatCrush Security Scan

25 finding(s)

HIGH/CRITICAL: 4 | MEDIUM: 12 | LOW: 9

Severity Rule Location
HIGH sh-remote-script-execution root-ubuntu.sh:3227
HIGH sh-remote-script-execution root-ubuntu.sh:3228
HIGH sh-remote-script-execution root-ubuntu.sh:4840
HIGH sh-remote-script-execution root-ubuntu.sh:4844
MEDIUM sql-template-interpolation dev2/dev2-site:754
MEDIUM sql-template-interpolation dev2/dev2-site:831
MEDIUM sh-remote-script-execution root-ubuntu.sh:4993
MEDIUM redos-nested-quantifier src/domain-free.ts:56
MEDIUM redos-nested-quantifier src/emoji.ts:167
MEDIUM redos-nested-quantifier src/icon.ts:166
MEDIUM redos-nested-quantifier src/mail.ts:1042
MEDIUM sql-template-interpolation src/users-dump.ts:487
MEDIUM sql-string-concatenation src/users-dump.ts:507
MEDIUM sql-template-interpolation src/users-dump.ts:540
MEDIUM sql-string-concatenation src/users-dump.ts:574
MEDIUM redos-nested-quantifier src/wcag.ts:556
LOW secret-generic-credential src/credentials.ts:36
LOW secret-generic-credential src/user-export.ts:632
LOW secret-generic-credential src/user-export.ts:638
LOW secret-generic-api-key test/credentials.test.ts:208
LOW secret-generic-credential test/mail.test.ts:141
LOW secret-generic-credential test/shorten.test.ts:36
LOW secret-database-url test/users-dump.test.ts:108
LOW secret-database-url test/users-dump.test.ts:119
LOW secret-database-url test/users-dump.test.ts:120

Snippets are redacted; ThreatCrush never prints matched credential material.

…s re-pointed once a database exists

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site Fixed
Comment thread dev2/dev2-site
the app's Postgres schema is applied by the app or by `libsql-pg convert-schema`)."""
s = site(args.site); st = load_state(s["site"])
db = db_name_for(s); pw = cluster_pg_password()
if pg_admin(f"select count(*) from pg_database where datname='{db}'").strip() == "0":
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread dev2/dev2-site Fixed
…rops long streams)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fleet kit wrote the values it re-points straight into
`sites.d/<site>.json`, so this pull request carried threatcrush.com's
Supabase service_role key, its anon key and its database password, plus
tipoffwatch.com's retired Railway Postgres URL -- in a public repository.
ThreatCrush was right to fail the branch.

`env_overrides` now takes `secret:<KEY>`, resolved at render time from
~/.config/cli-tools/credentials.json (0600, outside every checkout) under
`<site>.<KEY>`, falling back to a bare `<KEY>` for a value shared across
sites. A reference with nothing behind it stops provision and names the
key, rather than deploying a site whose environment contains the literal
string `secret:...`.

`supabase-cutover` was the thing writing them, so it now writes the
reference and puts the value in credentials.json. It marks what is secret
by provenance -- the state keys it substituted -- with a key-name pattern
as the backstop, because a wrong guess there costs one extra entry in
credentials.json and the opposite costs a published key.

Also:
  - the four values above moved into credentials.json on dev1; provision
    renders exactly what it rendered before (verified by resolving each
    reference back to the value this commit removes)
  - `porkbun_keys` and the Supabase SMTP lookup hand-rolled this same
    read twice; both now call `cred()`
  - `db_name_for` validates the identifier at the one place it is
    derived: `db_name` in sites.d reaches `create database` and a DSN
    unquoted
  - drop the committed `__pycache__/*.pyc` and ignore it

The keys this branch published still need rotating; removing them here
does not unpublish them.
ralyodio and others added 4 commits September 25, 2026 13:03
…SE when Railway set none

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…is merged

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ralyodio added a commit to profullstack/threatcrush that referenced this pull request Sep 25, 2026
Third false positive in the same family as #203: a pattern matching the
shape of a dangerous call without asking whether the danger is reachable.

py-ssrf-outbound-request fires on `urlopen(req)` for any variable `req`,
so a Python tool that talks to exactly two fixed APIs was reported twice
at high severity:

    req = urllib.request.Request("https://backboard.railway.com/graphql/v2", data=body)
    req = urllib.request.Request(f"https://api.supabase.com/v1/{path}")

SSRF is control of the destination, not of the path. Neither of those can
be steered to the metadata endpoint or to localhost for any input, and
the first has no interpolation in it at all.

`constantRequestHostGuard` resolves the variable to its assignments in
the file and exonerates only when every one of them names a literal
host. An interpolated authority (`f"https://{host}/v1"`), a bare
variable (`Request(url)`), or a name with no assignment in sight all
leave the finding standing, and a name assigned in two functions is
exonerated only if both destinations are fixed.

Measured on profullstack/cli-tools#103 with the language resolved the
way the CLI resolves it -- by shebang, since the file is extensionless:
4 findings -> 2, and the two that go are exactly the two GitHub reported
at dev2/dev2-site:136 and :1132. The two that remain are the real
interpolated SQL statements.
ralyodio and others added 5 commits September 25, 2026 13:12
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he Turso second pass

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… default

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 62648bc into master Sep 25, 2026
5 checks passed
@ralyodio
ralyodio deleted the ops/dev2-fleet-kit branch September 25, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants