Add the seed hub at seed.pyrtan.com, deployed on Fly.io
Claude · 2026-10-27 · 9f29c1781c9c0c42d4496dd7a8a27c869806addd · parents ad3c1f6
seeds.json was empty, so the landing page's directory had nothing to ask and `pyrtan follow` with no URL had nothing to follow. deploy/fly holds a federated, listed hub that never sleeps, keeps its follows and directory on a volume, and holds only the examples that read public APIs, with --egress public and a token. Its image is the root Dockerfile's, except that it hands the root-owned Fly volume to the pyrtan user before dropping to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gddz8MgVYLGb2iZ1cRkNdE
Changes
deploy/README.md | 3 ++- deploy/fly/Dockerfile | 23 +++++++++++++++++++++++ deploy/fly/README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ deploy/fly/fly.toml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ site/rendezvous/seeds.json | 4 +++- 5 files changed, 130 insertions(+), 2 deletions(-)
diff --git a/deploy/README.md b/deploy/README.md index 90721da..631cab7 100644 --- a/deploy/README.md +++ b/deploy/README.md@@ -1,7 +1,8 @@# Running the demo on OpenMediaVault Two ways. Docker through OMV's Compose plugin is the one that fits the box;-the systemd unit is for running it bare on the host.+the systemd unit is for running it bare on the host. (The seed hub at+`seed.pyrtan.com` runs on Fly.io instead; see [`fly/README.md`](fly/README.md).)Replace `<nas-ip>` below with your NAS's address on the LAN. Both end with the demo page at `http://<nas-ip>:8700/` (Docker) or `http://<nas-ip>:8701/` diff --git a/deploy/fly/Dockerfile b/deploy/fly/Dockerfile new file mode 100644 index 0000000..a7cdf51 --- /dev/null +++ b/deploy/fly/Dockerfile@@ -0,0 +1,23 @@+# Pyrtan's image for Fly.io: the root Dockerfile's, but it starts as root+# just long enough to hand the volume at /data to the pyrtan user, since a+# Fly volume mounts root-owned, then runs pyrtan as that user. Build it from+# the repository root; see README.md beside this file.+FROM python:3.12-slim AS build+WORKDIR /src+COPY pyproject.toml README.md LICENSE ./+COPY src ./src+RUN pip install --no-cache-dir --prefix=/install .++FROM python:3.12-slim+LABEL org.opencontainers.image.title="pyrtan" \+ org.opencontainers.image.source="https://github.com/b-i-t-8/pyrtan" \+ org.opencontainers.image.licenses="AGPL-3.0-or-later"+COPY --from=build /install /usr/local+COPY examples /examples+RUN chmod -R a+rX /examples \+ && useradd --system --uid 10001 --home /nonexistent --shell /usr/sbin/nologin pyrtan \+ && mkdir -p /data && chown pyrtan:pyrtan /data+ENV PYTHONUNBUFFERED=1+EXPOSE 8700+ENTRYPOINT ["sh", "-c", "chown pyrtan:pyrtan /data && exec setpriv --reuid=pyrtan --regid=pyrtan --init-groups --no-new-privs pyrtan \"$@\"", "pyrtan"]+CMD ["hub", "--host", "0.0.0.0", "--port", "8700"]diff --git a/deploy/fly/README.md b/deploy/fly/README.md new file mode 100644 index 0000000..8f3e027 --- /dev/null +++ b/deploy/fly/README.md@@ -0,0 +1,52 @@+# The seed hub on Fly.io++`seed.pyrtan.com` is the first seed in+[`site/rendezvous/seeds.json`](../../site/rendezvous/seeds.json): the hub+`pyrtan follow` with no URL follows first, and the one the landing page+asks for the directory. It is an ordinary federated hub, started+`--listed` and `--cors https://pyrtan.com`, that never sleeps, keeps its+follows and directory on a volume, and holds the examples that only read+public APIs (weather, hacker-news, github) with `--egress public`. The+other examples stay off it: they run a shell, read files or a serial+line, or fetch any host.++Everything is in [`fly.toml`](fly.toml). The image is the root+Dockerfile's with one change: it starts as root to hand the volume at+`/data` to the `pyrtan` user (a Fly volume mounts root-owned), then runs+as that user with no new privileges.++## First deploy++From the repository root, with [`flyctl`](https://fly.io/docs/flyctl/)+signed in:++```+fly apps create pyrtan-seed+fly volumes create pyrtan_seed --app pyrtan-seed --region iad --size 1 --yes+fly secrets set --app pyrtan-seed --stage PYRTAN_TOKEN="$(openssl rand -hex 24)"+fly deploy . --config deploy/fly/fly.toml --dockerfile deploy/fly/Dockerfile+fly ips allocate-v4 --shared --app pyrtan-seed+fly ips allocate-v6 --app pyrtan-seed+fly certs add seed.pyrtan.com --app pyrtan-seed+```++Then at the DNS provider, `CNAME seed -> pyrtan-seed.fly.dev`, and+`fly certs check seed.pyrtan.com --app pyrtan-seed` until it says the+certificate is issued. Keep the token: it is what `pyrtan gaps`,+`unanswered` and the console need to see more than `/actor` and+`/directory`, which stay open without it, as do signed requests from+federated hubs.++Check it:++```+curl https://seed.pyrtan.com/health+curl https://seed.pyrtan.com/directory+pyrtan directory+```++## Updating++`fly deploy . --config deploy/fly/fly.toml --dockerfile deploy/fly/Dockerfile`+from a checkout of `main`. The volume keeps who the seed follows and is+followed by, the directory, its blobs and its unanswered sentences.diff --git a/deploy/fly/fly.toml b/deploy/fly/fly.toml new file mode 100644 index 0000000..4b593a0 --- /dev/null +++ b/deploy/fly/fly.toml@@ -0,0 +1,50 @@+# The seed hub at seed.pyrtan.com, the first entry in+# site/rendezvous/seeds.json: a federated hub that is always up, lists+# itself in the directory, and holds the examples that only read public+# APIs. Deploy from the repository root:+#+# fly deploy . --config deploy/fly/fly.toml --dockerfile deploy/fly/Dockerfile+#+# First time, and the token, in README.md beside this file.++app = "pyrtan-seed"+primary_region = "iad"++[env]+ PYRTAN_ADVERTISE = "https://seed.pyrtan.com"+ PYRTAN_LISTED = "true"+ PYRTAN_REPORT_GAPS = "true"+ PYRTAN_STATE = "/data/federation.json"+ PYRTAN_STORE = "/data/store"+ PYRTAN_UNANSWERED = "/data/unanswered.jsonl"++# Only the trees that read public APIs: the others run a shell, read files+# or a serial line, or fetch any host, none of which a public box should do+# for strangers. PYRTAN_TOKEN is a secret (fly secrets set); /actor,+# /directory and signed federation requests stay open without it.+[processes]+ app = "hub --host 0.0.0.0 --port 8700 --name seed --trees /examples/weather --trees /examples/hacker-news --trees /examples/github --gaps /data/gaps.jsonl --egress public --cors https://pyrtan.com"++[[mounts]]+ source = "pyrtan_seed"+ destination = "/data"++[http_service]+ internal_port = 8700+ force_https = true+ # A seed answers follows and gossips the directory; it never sleeps.+ auto_stop_machines = "off"+ auto_start_machines = true+ min_machines_running = 1+ processes = ["app"]++ [[http_service.checks]]+ method = "GET"+ path = "/health"+ interval = "30s"+ timeout = "5s"+ grace_period = "10s"++[[vm]]+ size = "shared-cpu-1x"+ memory = "256mb"diff --git a/site/rendezvous/seeds.json b/site/rendezvous/seeds.json index 0b17941..9a63b00 100644 --- a/site/rendezvous/seeds.json +++ b/site/rendezvous/seeds.json@@ -1,5 +1,7 @@{ "pyrtan-seeds": 1, "about": "Hubs to follow first. `pyrtan follow` with no URL takes the first that answers /actor. Add yours by pull request to the site repository; a seed is any federated hub whose operator keeps it up and started it with --listed and --cors https://pyrtan.com.",- "hubs": []+ "hubs": [+ "https://seed.pyrtan.com"+ ]}