Run your own relay so AltSendme transfers do not use the public iroh relay infrastructure. Relays are stateless connection facilitators — all file data stays end-to-end encrypted.
- Deploy a relay using the assets in this directory (Docker Compose on a VPS or Fly.io).
- In the app, open Settings → Network and choose Custom self-hosted.
- Add your relay URL(s) and optional auth token if you enabled
access.shared_tokenon the server. - Use Test connection to verify registration.
For a fully private setup, configure the same relay URLs on both sender and receiver devices.
Transfers can still work when one side uses custom relays and the other uses the default public relays. Here's the simple version:
Your relay setting controls where your device registers. When you share a file, the ticket includes your relay URL. The other person connects using that ticket — they don't need to match your settings.
| Who shares | Sender uses | Receiver uses | Usually works? |
|---|---|---|---|
| Alice | Custom (open relay) | Public relays | Yes — receiver reaches Alice via the relay URL in the ticket |
| Alice | Custom (auth token required) | Public relays, no token | Often no — receiver can't authenticate to Alice's private relay |
| Alice | Custom (auth token required) | Same relay + same token | Yes |
| Either side | Any | Any, same LAN or good NAT | Yes — direct peer-to-peer may skip relays entirely |
Direction matters for privacy, not just connectivity:
- You share, they use public relays: If a relay is needed, traffic may go through your relay. They still use public relays for their own device.
- They share, you use a self-hosted relay: If a relay is needed, traffic may go through their public relay — yours isn't used for that path.
So mixed setups are fine for getting files across, but they're not fully private unless both people use the same self-hosted relay(s) (or connect directly without relay fallback).
- Just want it to work? An open self-hosted relay (no auth token) is enough; the other person can keep default public relays.
- Want a private relay? Both people need your relay URL and the auth token in Settings → Network.
- Want zero public relay use? Both people must set Custom self-hosted to the same relay(s).
- Want no relays at all? Both people set Disabled — only works when a direct connection is possible (e.g. same network).
| Requirement | Details |
|---|---|
| Server | VM or container with a public IP |
| DNS | A / AAAA record for your relay hostname |
| Ports | 80/tcp, 443/tcp, 7842/udp (QUIC address discovery). 9090/tcp is metrics — keep it private (see Observability) |
| TLS | Automatic via Let's Encrypt (built into iroh-relay) |
For production, run at least two relays in different regions and add both URLs in AltSendme → Settings → Network.
Fly supports UDP and raw ports, which many PaaS providers do not.
cd deploy/relay
cp iroh-relay.conf.example iroh-relay.conf
# Edit hostname and contact
fly launch --no-deploy
fly volumes create relay_certs --size 1 --region <your-region>
# Private relay only — set the token as a secret, never bake it into the image:
fly secrets set IROH_RELAY_ACCESS_TOKEN=$(openssl rand -hex 32)
fly deployUpdate iroh-relay.conf so hostname matches the DNS name you point at the Fly app.
For a fast functional test without owning a domain, use fly.dev.toml. It runs the relay
in --dev mode (plain HTTP on port 3340); Fly's edge terminates TLS and proxies to it, so
the relay is reachable at https://<app>.fly.dev with a valid cert.
cd deploy/relay
# edit fly.dev.toml: set a unique `app` name and a nearby `primary_region`
fly apps create <your-unique-name>
fly deploy --config fly.dev.toml
fly status # confirm the machine is runningThen in AltSendme → Settings → Network → Custom self-hosted, add
https://<your-unique-name>.fly.dev and click Test connection.
Caveat: this mode provides relaying only — QUIC address discovery / holepunch assist is disabled in
--dev(it needs direct UDP + TLS). It's ideal for trying the feature, not for a production relay. For production, use the Let's Encrypt setup above (fly.toml+ your own domain).
-
Copy and edit the config:
cd deploy/relay cp iroh-relay.conf.example iroh-relay.conf # Set hostname, contact email, and review the [limits] block
iroh-relay.confis gitignored, so it is safe to keep per-deployment values there. Keep the access token out of the file — pass it via the environment instead (see Private relay). -
Point DNS at your server.
-
Start:
docker compose up -d
-
In AltSendme → Settings → Network, choose Custom self-hosted, add
https://euc1-1.relay.example.com(see Region naming), and paste your auth token if you enabled access control.
An open relay with no caps lets a single client run up surprise terabyte-scale egress.
iroh-relay disables rate limiting entirely when the [limits] block is absent, so the
shipped iroh-relay.conf.example includes sane defaults:
[limits]
accept_conn_limit = 100.0 # new connections/sec (parsed but NOT yet enforced in v1.0.0)
accept_conn_burst = 100 # (parsed but NOT yet enforced in v1.0.0)
[limits.client.rx]
bytes_per_second = 1048576 # 1 MiB/s per client, steady state (enforced)
max_burst_bytes = 5242880 # 5 MiB burst bucket (enforced)Heads up: as of
iroh-relayv1.0.0 theaccept_conn_limit/accept_conn_burstconnection-rate knobs are accepted by the parser but not yet implemented upstream, so they currently do nothing. The per-client[limits.client.rx]block is the control that actually caps bandwidth today — make sure you set it.
Tune these to your hardware and bill tolerance. bytes_per_second is required whenever
max_burst_bytes is set, and both must be non-zero.
access is a single setting — pick at most one tier. With none set, the relay is open
to everyone (access = "everyone").
Simplest private relay. Prefer the environment variable over the config file so the secret
never lands in an image layer or a commit. When an access.shared_token line is present,
IROH_RELAY_ACCESS_TOKEN replaces it at startup — a placeholder in the file is fine.
In iroh-relay.conf:
access.shared_token = ["placeholder-overridden-by-env"]Then provide the real value out-of-band:
# Docker Compose (local .env file, gitignored):
echo "IROH_RELAY_ACCESS_TOKEN=$(openssl rand -hex 32)" >> deploy/relay/.env
# Fly.io:
fly secrets set IROH_RELAY_ACCESS_TOKEN=$(openssl rand -hex 32)Use the same value in AltSendme → Settings → Network → Auth token on every device.
Static tokens have no expiry and no per-client revocation — rotating one means updating every client. For revocation without restarts, use Tier 3.
Identity-bound and more granular than a shared secret, with no token in the app. Gate by hex endpoint id:
access.allowlist = ["<endpoint-id>", "<endpoint-id>"]
# or
access.denylist = ["<endpoint-id>"]The relay POSTs each connecting endpoint id (header X-Iroh-NodeId) to your auth service,
which must reply 200 with the body true to allow. This gives you live revocation and
rotation without editing config or restarting:
access.http.url = "https://auth.example.com/relay-auth"
# Optional bearer token to your auth service (or set IROH_RELAY_HTTP_BEARER_TOKEN):
access.http.bearer_token = "your-callout-token"iroh-relay serves Prometheus metrics on :9090 by default, with no authentication.
Do not expose this port to the public internet.
- Docker Compose: the bundled
docker-compose.ymlbinds it to127.0.0.1:9090only. - Fly.io:
fly.tomluses a private[metrics]block scraped over Fly's internal network instead of a public port.
Scrape it from your own Prometheus (over a private network / VPN / firewall allowlist):
scrape_configs:
- job_name: iroh-relay
static_configs:
- targets: ["10.0.0.5:9090"] # private address of your relayTo turn metrics off entirely, set enable_metrics = false in iroh-relay.conf (and remove
the compose healthcheck, which probes /metrics).
For uptime monitoring the relay also serves a built-in /healthz endpoint (200 + JSON
status) on its main HTTP(S) port — https://relay.example.com/healthz in production, or
http://<host>:3340/healthz in --dev mode.
iroh-relay's Let's Encrypt mode wants ports 80 and 443 for the ACME challenge and TLS,
which collides with an existing Traefik / Caddy / nginx on the same host. Options:
- Give the relay its own host/IP (or a dedicated machine) so it owns 80/443. Simplest for a production relay; full QUIC hole-punch assist works.
- Terminate TLS at your proxy and run the relay in
--devmode (plain HTTP on:3340), exactly like the Fly "no domain" path above. Proxyhttps://relay.example.com→http://127.0.0.1:3340. Caveat:--devdisables QUIC address discovery, so you get relaying only (no hole-punch assist).
The official image runs as root, which is what lets it bind 80/443. To run unprivileged:
-
Grant just the bind capability instead of full root:
# docker-compose.yml cap_add: - NET_BIND_SERVICE user: "65532:65532"
(Ensure
/data/certsis writable by that uid.) -
Or run on high ports behind a reverse proxy (see above) so no privileged bind is needed.
To keep the Compose stack running across reboots without Docker's own restart policy, drop
this unit at /etc/systemd/system/iroh-relay.service:
[Unit]
Description=AltSendme iroh-relay
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/alt-sendme/deploy/relay
EnvironmentFile=-/opt/alt-sendme/deploy/relay/.env
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now iroh-relayNo upstream NixOS module exists yet; run the container declaratively with
virtualisation.oci-containers:
{
virtualisation.oci-containers.containers.iroh-relay = {
image = "n0computer/iroh-relay:v1.0.0";
cmd = [ "--config-path" "/config/iroh-relay.conf" ];
ports = [ "80:80" "443:443" "7842:7842/udp" "127.0.0.1:9090:9090" ];
volumes = [
"/etc/iroh-relay/iroh-relay.conf:/config/iroh-relay.conf:ro"
"iroh-relay-certs:/data/certs"
];
# Keep the token out of the Nix store — load it from an agenix/sops secret:
environmentFiles = [ "/run/secrets/iroh-relay.env" ]; # IROH_RELAY_ACCESS_TOKEN=...
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.firewall.allowedUDPPorts = [ 7842 ];
}After deployment, open AltSendme → Settings → Network → Test connection. A successful test confirms the app can register with your relay.
- ACME / TLS fails: ensure port 80 is reachable from the internet and DNS points to this host. If something else owns 80/443, see Running behind an existing reverse proxy.
- Test connection times out: check firewall rules for 443/tcp and 7842/udp.
- Auth fails: confirm the effective token (config
access.shared_tokenorIROH_RELAY_ACCESS_TOKEN) matches the token in the app. Remember the env var overrides the file. - Relay won't start with a token set: an empty
IROH_RELAY_ACCESS_TOKEN(or an empty string inaccess.shared_token) is rejected at startup. Leave the env var unset if you are not using shared-token auth.
AltSendme shows a country flag next to a relay URL when the hostname starts with an
AWS/iroh-style region code (e.g. euc1 → Frankfurt, use1 → US East, aps1 → Mumbai).
Following the https://<region>-<n>.relay.example.com convention is purely cosmetic and
never required.