---
title: "Run a host of your own"
url: https://docs.loot.build/guides/run-a-host
group: "Guides"
status: written
summary: "A relay anywhere, with an allowlist you control."
---

# Run a host of your own

> A relay anywhere, with an allowlist you control.

The relay above dies with the terminal. A _host is just a relay that never sleeps_, so the difference is a service unit, a push allowlist and TLS — not different software. `loot serve` is in the CLI you already installed.

## Start it once by hand

`--allow` takes a pubkey in either form you can actually hold: the OpenSSH line `loot whoami --pubkey` prints, or its 64-hex equivalent. Repeat it once per pusher.

```bash
loot serve --dir /opt/loot/relay --addr 127.0.0.1:4000 \
  --allow "$(loot whoami --pubkey)"

# in another terminal — the relay reports the lists it parsed:
curl -s http://127.0.0.1:4000/info
```

`/info` echoes your key back as 64 hex under`allowed_pubkeys`. That is the same key in the other encoding, and it is the form to paste into the unit file below — a systemd`ExecStart` splits on whitespace, and the OpenSSH line contains spaces.

⚠ Pass `--allow` at least once. **An empty push allowlist means an open relay**: every validly signed push is accepted, and every read is served to anyone who can reach the relay, **internal content included**, because a relay holds the internal keys (ADR 0011). With a list, only listed keys may read as well as push. It opens the grant gate with it — the grant allowlist defaults to the push one, and `--allow-grant` replaces that default rather than adding to it. Open is the right posture for a throwaway on `127.0.0.1` and the wrong one for anything reachable; the startup banner names which posture you got, every time. Only _omitting_ the flag opens the relay: an `--allow` whose key went missing — a truncated paste, an unfilled template variable — refuses to start rather than inverting what you asked for.

## Make it survive a reboot

```bash
# /etc/systemd/system/loot-relay.service
[Unit]
Description=loot relay (zero-knowledge push/pull host)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/loot serve --dir /opt/loot/relay --addr 127.0.0.1:4000 --allow <64-hex>
Restart=on-failure
RestartSec=2
# The relay never needs to read outside its data dir; keep it contained.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/loot/relay
ProtectHome=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

Then `systemctl daemon-reload && systemctl enable --now loot-relay`. This is the unit loot’s own relay ran under, hardening included — the relay writes nothing outside its data dir, so`ProtectSystem=strict` costs it nothing.

Bind to `127.0.0.1` and terminate TLS in front of it — nginx and Let’s Encrypt, with only nginx exposed. The relay speaks plain HTTP and should never be the public listener.

## What you are signing up for

⚠ **The store only grows, and has no ceiling.** A relay never forgets a change, so everything it holds stays reachable — and nothing reachable may be reclaimed. The only drains are a `gc` over torn writes and a reap of undelivered grants, which is genuine garbage rather than history. A relay is measured, not capped (ADR 0048): watch the volume, because it will not level off on its own.

⚠ **A relay is not blind to everything.** It can never read restricted content, or embargoed content before its reveal — those keys are never sealed to it. It _can_ read **internal** content: those keys ride along so that peers pulling from it get readable files at all (ADR 0011). Run a relay for people you would hand internal files to, or mark what matters restricted.

What this is not: a relay is not the forge. No accounts, no web UI, no browser read path, and no key retirement — a relay gates pushes and reads on its push allowlist alone, so rotating a key on this site does not stop that key pushing to somebody’s relay. Hosting on the forge at `loot.build` is invite-only by construction; this is the path that needs no invitation.
