Self-hosting
Run your own Seed site on a Linux server with the deploy script, register your space from the Seed app, and keep it updated, backed up and reachable on your domain.

A Seed site is a Seed daemon and a Seed web app behind a reverse proxy, serving one space at a web domain. The hosted service runs sites for you under *.hyper.media. This guide runs the same three containers on a server you control. Your content and your key stay yours either way, so a hosted site can move to your own server later and back again.

Goal. A site at https://example.org that publishes your space, gets TLS certificates by itself, checks for updates every ten minutes, and can be backed up in one command.

Prerequisites.

    A Linux server with about 2 GB of RAM, a public IPv4 address, and ports 80 and 443 free. The deploy script needs glibc 2.25 or newer, which means Ubuntu 18.04, Debian 10, RHEL 8, Fedora 28 or Amazon Linux 2023 and later. It installs Docker and Bun if they are missing.

    A domain name you control, with access to its DNS records.

    The Seed desktop app with the space you want to publish, and its key. Whoever holds that key can publish to the site.

1. Point the domain at the server

In your DNS provider, add an A record for the domain (the name is @ for the root, or a subdomain such as docs) whose value is the server's IP address. Do this first. The proxy requests a certificate from Let's Encrypt on first start, and that only succeeds once the name resolves to the server.

2. Run the deploy script

Log in as root or as a user with sudo, and run:

curl -fsSL https://deploy.seed.hyper.media | sh

At the time of writing that URL redirects to the bootstrap script in the Seed repository, ops/deploy.sh on the main branch. You can fetch it from GitHub directly if you want to read it first. The bootstrap installs Docker and Bun when needed, downloads the deployment engine to /usr/local/lib/seed/deploy.js, installs a seed-deploy command, and starts an interactive wizard.

The wizard asks:

Question

What to answer

Public hostname

the full URL, https://example.org

P2P network

Mainnet, unless you are testing against the team's dev network

Release channel

Stable (latest) for the released images; Bleeding edge (dev) tracks main

Log level

Info

Gateway mode

No, for a site that publishes one space; Yes makes the node serve every public account it learns about, like hyper.media

Contact email

optional, for security notices

It then writes the configuration, generates a Caddyfile, pulls the images and starts the containers. On a first deploy it ends with:

Setup complete Your site is live at https://example.org Registration URL: https://example.org/hm/register?secret=… Copy this URL and paste it into the Seed desktop app to link your publisher account to this site.

Until a space is registered the site renders a "not registered" page. seed-deploy secret prints the registration URL again.

3. Register your space from the Seed app

In the desktop app, open the space, choose Publish Site from the options menu at the top right, paste the registration URL and confirm. The app fetches the site's /hm/api/config, sends its account id and peer addresses to /hm/api/register with the secret, and pushes the space's home document with its related material to the site's daemon. The site records the account, subscribes its daemon to the space, and from then on renders that account's home document at https://example.org.

The secret is consumed on registration. A registered site refuses a different account. To move a site to another space, replace the web configuration file described below with {"availableRegistrationSecret": "<secret>"} (the secret from seed-deploy secret works), run seed-deploy restart so the web app rereads it, and register again.

What is running

Three containers, defined by the compose file the script downloads from the repository.

Container

Image

Ports on the host

Role

seed-proxy

caddy:2

80, 443, 443/udp

TLS from Let's Encrypt, then reverse proxy

seed-web

seedhypermedia/web

3000

the Seed web app: pages, the Seed API, site services

seed-daemon

seedhypermedia/site

56000 and 56000/udp

the Seed daemon: storage, indexing, libp2p

Caddy sends /ipfs/* to the daemon and everything else to the web app. The daemon's own HTTP and gRPC ports (56001 and 56002) are not published on the host. Only the web app reaches them over the container network. The daemon's API has no authentication of its own (see daemon gRPC), so keep it that way and do not open those ports in a firewall. The compose file does not pass -public-only to the daemon. On a site that holds private documents of the registered space, the web app decides what a visitor may see by forwarding their sign-in token to the daemon.

The daemon runs with -p2p.force-reachability-public and -p2p.no-relay, announcing /dns4/example.org/tcp/56000 and the QUIC equivalent, so the libp2p port must be reachable from the internet. Peers, including the desktop app that publishes to the site, connect to it directly.

Everything lives under one directory, /opt/seed by default.

Path

Contents

config.json

the wizard's answers: domain, testnet, release_channel, gateway, email, plus the registration link_secret and bookkeeping

docker-compose.yml

the compose file, refreshed on every deploy

proxy/CaddyFile, proxy/data, proxy/config

the proxy configuration and its certificates

web/config.json

the site's web configuration: availableRegistrationSecret before registration, registeredAccountUid and sourcePeerId after

web/image-cache/

resized images

daemon/

the daemon's data directory: the SQLite database, the blob store, keys/ with the node's libp2p identity, and a file keystore for the server signing key

Containers run as your user and never as root. All bind mounts carry the :z flag for SELinux hosts.

Day-to-day

The seed-deploy command manages the node.

Command

What it does

seed-deploy or seed-deploy deploy

headless update: refetch the compose file, pull images if anything changed, recreate containers, prune old images; a no-op when nothing changed

seed-deploy deploy --reconfigure

re-run the wizard with current values as defaults

seed-deploy doctor

health: containers, disk, cron, certificate expiry, whether the site is registered

seed-deploy logs daemon, logs web, logs proxy

tail a container's logs

seed-deploy secret, seed-deploy config

the registration URL; the configuration with secrets redacted

seed-deploy stop, start, restart

without redeploying

seed-deploy backup [path]

a .tar.gz of the configuration and the web, daemon and proxy directories; containers stop during the backup and restart after

seed-deploy restore <file>

unpack a backup, optionally edit the configuration, deploy

seed-deploy cron, cron remove

install or remove the update jobs

seed-deploy upgrade

update the deploy script itself

seed-deploy uninstall

remove containers, data and configuration

Updates. The wizard installs two cron jobs. Every ten minutes it runs upgrade then deploy, so the node follows its release channel. Every hour it prunes unused images older than an hour. The deploy script itself always tracks the main branch, independent of the image channel, so fixes to the orchestration reach every node. SEED_DEPLOY_URL and SEED_REPO_URL redirect the source for testing a branch.

Which version is running. https://example.org/hm/api/version returns the commit, branch and build date of both the web app and the daemon.

Testing a branch. seed-deploy --advanced adds two things: choosing the node directory, so a branch build with its own database migrations lives in /opt/seed-mybranch beside your main node, and a custom image tag. Only one node runs at a time per host, because the container names are fixed. deploy and start refuse to replace a stack owned by another directory, and doctor warns when they disagree.

Metrics. The compose file has a metrics profile with Prometheus and Grafana, served at https://example.org/.metrics when enabled.

Debug pages. The daemon's /debug/* pages answer only to loopback inside its container. docker exec seed-daemon gets you a shell there. seed-deploy logs daemon is usually enough.

Custom domains for hosted sites

If your site is on the hosted service at yoursite.hyper.media, you can still serve it at your own domain without self-hosting. In the Seed app, open the site, choose Publish Custom Domain from the options menu, enter the domain and confirm. Then in DNS either add an ALIAS or flattened CNAME record pointing at yoursite.hyper.media (turn off proxying if you use Cloudflare), or, when your provider cannot do that, an A record pointing at the hosted service's address. At the time of writing hyper.media resolves to 40.160.6.196; check with dig +short A hyper.media before you copy it. Keep the app open while DNS propagates, usually within ten minutes, and the site goes live on the new domain.

The legacy script

website_deployment.sh at the repository root is the previous installer. It is deprecated, with a notice in its header. The deploy script detects installations it made, migrates their configuration, and removes the Watchtower auto-updater they used. Do not use it for new sites.

Where this is going

As of September 2026, the deploy script does not yet report new deployments to the Seed team (a TODO in the source), so security notices depend on the email you entered. A guide for self-hosting behind a different domain setup than a single A record, and for running a node without publishing a site, is not written. The hosted service's own infrastructure (multi-tenant sites, custom domains at scale) lives in a separate repository and is not documented here.

See also

    Sites, what a site is in the protocol: the home document's siteUrl, registration, gateways

    Network, how the site's daemon syncs with the app that publishes to it

    Seed API, everything your new server answers

    Web app and daemon, configuration and flags in detail

    Keys, the key that publishes to the site

    Contributing, for the ops/ deploy tooling

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime