From 28c881ba97b6b7d774a34fb4668429a93f6fdb4b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 20 Aug 2026 19:24:19 +0000 Subject: [PATCH] feat(deploy): add managed hosting templates Agents need repeatable provider inputs instead of translating a generic container guide into mutable dashboard settings on every deployment. Add Railway, Render, and Fly.io configurations that all build the canonical Dockerfile, mount /data, expose one HTTP service, allow bounded shutdown, and avoid overlapping writers. Pair them with provider-specific CLI and dashboard instructions, DNS verification, backup cautions, and one-instance constraints. The templates assume each provider terminates TLS and preserves WebSocket upgrades. Render still requires a supported connected Git source for Blueprint automation, and provider credentials or account mutations are deliberately outside this commit. Validated Railway and Render against their current published JSON schemas, parsed both TOML files and all YAML, checked Fly fields against its current official reference, verified local documentation links, and ran git diff --check. flyctl strict validation was not possible without a Fly access token. --- docs/how-to/deploy-paas.md | 162 +++++++++++++++++++++++++++++++++++++ fly.toml.example | 42 ++++++++++ railway.toml | 12 +++ render.yaml | 19 +++++ 4 files changed, 235 insertions(+) create mode 100644 docs/how-to/deploy-paas.md create mode 100644 fly.toml.example create mode 100644 railway.toml create mode 100644 render.yaml diff --git a/docs/how-to/deploy-paas.md b/docs/how-to/deploy-paas.md new file mode 100644 index 0000000..157568c --- /dev/null +++ b/docs/how-to/deploy-paas.md @@ -0,0 +1,162 @@ +# Deploy on managed hosting + +The supplied container can run on Railway, Render, and Fly.io when each service +has a custom domain, one persistent `/data` volume, and exactly one running +instance. These platforms terminate TLS and proxy WebSockets to ngit-grasp. + +Managed hosting is best for a small or moderate relay whose operator accepts +brief upgrade downtime. For a large existing relay, use NixOS, systemd, or +Compose so startup migrations and storage snapshots remain under direct +operator control. + +Read the [deployment contract](../reference/deployment-contract.md) first. In +particular, provider replicas do not make ngit-grasp highly available: their +local volumes do not replicate application state. + +## Common requirements + +For every provider: + +- set `NGIT_DOMAIN` to the final custom hostname, not the provider hostname; +- mount durable storage at `/data` before the first successful start; +- keep the instance count at one and disable scale-to-zero; +- keep `NGIT_BASE_PATH=/` unless path routing has been designed explicitly; +- allow at least five minutes between `SIGTERM` and forced termination; and +- take an external backup of `/data`, including `.relay-owner.nsec`. + +The image maps a platform-provided `PORT` to `0.0.0.0:${PORT}`. Do not set +`NGIT_BIND_ADDRESS` unless the provider template below does so explicitly. + +## Railway + +`railway.toml` selects the Dockerfile, uses `/` as the health check, disables +old/new deployment overlap, and gives shutdown five minutes. Railway storage is +configured separately from config-as-code. See Railway's +[config-as-code reference](https://docs.railway.com/config-as-code/reference) +and [volume guide](https://docs.railway.com/volumes) for the provider-side +details. + +From the repository root with the Railway CLI installed: + +```bash +railway login +railway init +railway add --service ngit-grasp +railway service ngit-grasp +railway variable set NGIT_DOMAIN=ngit.example.com NGIT_BASE_PATH=/ +railway volume add --mount-path /data +railway up +railway domain ngit.example.com +``` + +Add the DNS records returned by `railway domain`, then inspect status and verify +the public relay: + +```bash +railway domain status ngit.example.com +railway deployment list --json +scripts/verify-deployment.sh https://ngit.example.com +``` + +Do not add replicas or multi-region configuration. Railway mounts a new volume +as root; the image entrypoint prepares it and drops privileges before starting +ngit-grasp. + +Railway volume backups are useful recovery points, but also export or snapshot +state outside the platform. During a large storage migration, increase the +health-check timeout or move the upgrade to a directly operated host. + +## Render + +`render.yaml` describes a paid `starter` web service with a 10 GB disk mounted +at `/data`, a root health check, a five-minute shutdown delay, and automatic +deploys disabled. Render prompts for `NGIT_DOMAIN` when the Blueprint is +created. The fields follow Render's +[Blueprint specification](https://render.com/docs/blueprint-spec); its +[persistent disk guide](https://render.com/docs/disks) describes the storage +and scaling constraints. + +Render Blueprints require a repository connected through one of Render's +supported Git providers. Until ngit-grasp has an authorized mirror or published +OCI image, use Render's **Public Git Repository** flow with the canonical URL: + +```text +https://gitnostr.com/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git +``` + +Choose Docker and apply the values from `render.yaml` in the service form: + +- environment variable `NGIT_DOMAIN=ngit.example.com`; +- persistent disk mounted at `/data`; +- health check path `/`; +- maximum shutdown delay 300 seconds; and +- one instance with automatic deploys disabled. + +Add and verify the custom domain before publishing it in repository +announcements. Render services backed by a persistent disk cannot run multiple +instances and have brief downtime during deploys, which matches the relay's +single-writer requirement. + +After a manual deploy, run: + +```bash +scripts/verify-deployment.sh https://ngit.example.com +``` + +Use Render disk snapshots plus an independent backup. Only `/data` persists; +all other container filesystem changes are ephemeral. + +## Fly.io + +Fly volumes are local to one Machine and are not automatically replicated. The +template therefore disables autostop, uses an immediate replacement strategy, +and must be deployed with high-availability seeding disabled. See Fly's +[configuration reference](https://fly.io/docs/reference/configuration/) and +[volume overview](https://fly.io/docs/volumes/overview/) for the underlying +platform behavior. + +Copy and edit the template: + +```bash +cp fly.toml.example fly.toml +# Set a unique app name, primary region, and NGIT_DOMAIN in fly.toml. +fly config validate --strict --config fly.toml +``` + +Create the app and one volume in the same region, then deploy one Machine: + +```bash +fly apps create replace-with-a-unique-ngit-grasp-name +fly volumes create ngit_grasp_data --region ord --size 10 +fly deploy --ha=false +fly scale count 1 +fly certs add ngit.example.com +``` + +Use the app name and region selected in `fly.toml`, add the DNS records shown by +`fly certs`, then verify: + +```bash +fly checks list +fly scale show +scripts/verify-deployment.sh https://ngit.example.com +``` + +Never scale above one: Fly creates a separate empty volume for each additional +Machine and does not copy ngit-grasp state. A single Machine and volume can be +unavailable after host failure, so maintain an external backup rather than +relying only on Fly snapshots. + +## Upgrades + +Managed volume deployments cannot provide a safe zero-downtime writer handoff. +Expect brief downtime: + +1. disable automatic deploys; +2. take a complete provider snapshot and an external backup; +3. deploy the pinned revision with one instance; +4. inspect startup and integrity logs; and +5. run the public verifier. + +For a storage-changing release, restore both the prior executable and its +matching pre-upgrade `/data` snapshot when rolling back. diff --git a/fly.toml.example b/fly.toml.example new file mode 100644 index 0000000..26cb2d3 --- /dev/null +++ b/fly.toml.example @@ -0,0 +1,42 @@ +# Copy this file to fly.toml, then change the app, primary_region, and +# NGIT_DOMAIN values before creating the Fly app. +app = "replace-with-a-unique-ngit-grasp-name" +primary_region = "ord" +kill_signal = "SIGTERM" +kill_timeout = 300 + +[build] +dockerfile = "Dockerfile" + +[env] +NGIT_DOMAIN = "ngit.example.com" +NGIT_BASE_PATH = "/" +NGIT_BIND_ADDRESS = "0.0.0.0:7334" +NGIT_GIT_DATA_PATH = "/data/git" +NGIT_RELAY_DATA_PATH = "/data/relay" +NGIT_LOG_LEVEL = "info" + +[deploy] +strategy = "immediate" + +[http_service] +internal_port = 7334 +force_https = true +auto_stop_machines = "off" +auto_start_machines = true +min_machines_running = 1 + +[[http_service.checks]] +grace_period = "30s" +interval = "30s" +method = "GET" +timeout = "5s" +path = "/" + +[[mounts]] +source = "ngit_grasp_data" +destination = "/data" + +[[vm]] +size = "shared-cpu-1x" +memory = "1gb" diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..d668c25 --- /dev/null +++ b/railway.toml @@ -0,0 +1,12 @@ +"$schema" = "https://railway.com/railway.schema.json" + +[build] +builder = "DOCKERFILE" +dockerfilePath = "Dockerfile" + +[deploy] +healthcheckPath = "/" +healthcheckTimeout = 300 +restartPolicyType = "ALWAYS" +overlapSeconds = 0 +drainingSeconds = 300 diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..c5705fa --- /dev/null +++ b/render.yaml @@ -0,0 +1,19 @@ +services: + - type: web + name: ngit-grasp + runtime: docker + plan: starter + autoDeployTrigger: off + healthCheckPath: / + maxShutdownDelaySeconds: 300 + disk: + name: ngit-grasp-data + mountPath: /data + sizeGB: 10 + envVars: + - key: NGIT_DOMAIN + sync: false + - key: NGIT_BASE_PATH + value: / + - key: NGIT_LOG_LEVEL + value: info