Server-to-server transfers with the Relayium CLI (daemon direct)
Last updated: 2026-08-05
When both machines are yours and each knows the other's address, SSH is extra friction and a rendezvous is pure overhead. Daemon direct is built for exactly this: one server listens, the other pushes straight to it over a pinned TLS 1.3 connection. No relay, no SSH, no pairing code — trust is public-key and set up once.
This guide covers starting the listener, pushing to it, approving a new pusher on first contact, automating it, and running the listener as a systemd service.
Before you start
Everything below is the relayium CLI, so install it first if you haven't. On macOS or Linux, one command drops a prebuilt binary on your PATH:
curl -fsSL https://relayium.com/install.sh | sh
- Prefer to pick the file yourself, or on Windows? Grab a binary from the releases page — relayium.com/cli lists every install option (or go build -o relayium ./cmd/relayium if you have Go).
- relayium --version confirms it's installed. Skip this and the commands below just print 'command not found'.
Start the listener (on the receiver)
What you need
- Two machines you control, with the receiver's address reachable from the sender. A hostname or a bare IP both work.
- relayium on both ends. Daemon direct speaks the native protocol only, so there is no tar fallback to rescue a missing install here.
- The listener's port open to the sender — 9031/TCP unless you change it — in the host firewall and in any cloud security group.
- A terminal on the receiver for the first push, so you can answer the approval prompt. With no terminal, pre-authorize the sender instead (see below).
On the receiving server, serve listens for pushes and writes them into a directory. It's long-running by default; add --once to accept a single transfer and exit. You don't pre-share anything — no fingerprints to copy up front:
Create the directory pushes should land in.
mkdir -p ~/inboxOpen the listener's port to the sender only. Substitute the sender's own address for 203.0.113.7 — its public IP, or its private one when the two servers share a network — and scope the cloud security group to that same source rather than to the whole internet.
sudo ufw allow from 203.0.113.7 to any port 9031 proto tcpStart the listener in a terminal, so there is someone to answer the approval prompt on the first push. Add --once to take a single transfer and exit, or --port to move off 9031.
relayium serve --dir ~/inbox
What a running listener looks like
serve names the address it bound, the directory it writes into, and this host's own fingerprint. With no approved peers yet, it also says it will ask about each new one.
relayium serve --dir ~/inbox
no authorized peers yet — you'll be asked to approve each new peer on its first push.
relayium serve: listening on [::]:9031, receiving into /home/you/inbox (fingerprint 5c1d9f04…)- The listener processes connections one at a time and lands files under --dir.
- The default port is 9031; change it with --port and open it on your firewall.
Push to it (on the sender)
From the sending server, push to the receiver's relayium:// address. The first connection pins the receiver's fingerprint; every connection after that verifies it, and a changed fingerprint is refused rather than silently accepted — so a swapped key or a man-in-the-middle is caught, not trusted. On the very first push, the sender waits a moment while the receiver approves it (next step).
Run the push from the sending server. On the very first connection it stops right here, while the receiver approves it.
relayium push ./build.tar.zst relayium://receiver.example.comAnswer the prompt on the receiver — that is the next section. The push then finishes on its own, and later pushes never stop here again.
Append a port when the listener isn't on 9031.
relayium push ./build.tar.zst relayium://receiver.example.com:9040
What a successful push looks like
On first contact the sender learns and pins the listener's fingerprint, then transfers. The receiver records the pusher's fingerprint and reports the file and byte count.
# on the SENDER, first contact
learned receiver.example.com:9031 5c1d9f04… (added to known_hosts)
build.tar.zst (48213004 bytes)
# on the RECEIVER
authorized 74318e3b… (added to /home/you/.config/relayium/authorized_fingerprints)
received 1 file(s), 48213004 bytes from 74318e3b…- No relay and no fallback: if the listener isn't reachable, the push fails — file bytes never route through anyone else.
- The same transfer engine as the other modes: resumable, with a per-file SHA-256 check.
Approve the sender on first push (on the receiver)
The first time a new machine pushes to your listener, serve (in a terminal) shows you where it's from and its fingerprint and asks you to approve it — like SSH's first-connect prompt, but on the receiving side:
# on the RECEIVER, when a new sender pushes:
Incoming push from 203.0.113.7:54021
fingerprint: 74318e3b…
Accept and remember this peer? [y/N] y
- Answer y and that fingerprint is remembered in authorized_fingerprints; every later push from the same machine then goes through silently.
- The fingerprint is a machine's stable identity (it survives restarts and IP changes), so approving is a one-time step per pusher.
- The pusher, in turn, learns the listener's key on first connect (trust on first use) and pins it in known_hosts.
Automate it (or run without a terminal)
Because an approved fingerprint is remembered, later pushes need no prompt — so relayium push drops straight into cron, a deploy script, or CI for encrypted, integrity-checked, resumable server-to-server sync. When serve runs without a terminal (a systemd service, a pipe) it can't prompt, so it rejects unknown pushers; pre-authorize them instead. Get the fingerprint from the pusher's relayium id, or copy it from the "rejected unauthorized peer …" line in the serve log, then:
# on the RECEIVER: pre-authorize a sender without a prompt
relayium authorize 74318e3b...
- Identity and trust files live in ~/.config/relayium/ (override with --config-dir, e.g. /etc/relayium for a service).
- authorize is idempotent — running it again for the same fingerprint is a no-op.
Run the listener under systemd
For an always-on inbox, run serve as a systemd service. Point --config-dir at a fixed location like /etc/relayium so the identity is stable across restarts, and let systemd keep it alive:
# /etc/systemd/system/relayium-serve.service
[Unit]
Description=Relayium daemon-direct listener
After=network-online.target
[Service]
ExecStart=/usr/local/bin/relayium serve --dir /srv/inbox --config-dir /etc/relayium
Restart=always
User=relayium
[Install]
WantedBy=multi-user.target
- systemctl enable --now relayium-serve to start it and bring it up on boot.
- Keep /etc/relayium/id.key readable only by the service user — relayium refuses to load a key with loose permissions.
When a push doesn't get through
Reachability and trust are the first two things to check: ss -tinp on the sender says whether the listener was reached at all, and relayium authorize on the receiver grants the trust a rejected pusher is missing. They are not the only ways a push can fail — a receiver out of disk space, an inbox its user cannot write into, or a transferred file whose integrity check fails all report themselves — so read the error in front of you rather than assuming it is one of the four below.
Symptom, check, fix
- The push sits there, then fails with a connection error.
# on the SENDER, while the push is running — run it twice, a few seconds apart ss -tinp dst :9031 # ESTAB the listener was reached; says nothing about progress # SYN-SENT nothing answered on that portSYN-SENT means the packets never reached a listening socket. Confirm serve is up on the receiver with ss -tlnp | grep 9031, then open 9031/TCP to the sender in the host firewall and the cloud security group. ESTAB proves reachability only — an established socket can sit idle or stall — so to separate a live transfer from a stalled one, run the check twice a few seconds apart and compare the bytes_acked counter that -i prints for that socket. There is no relay path here, so an unreachable listener is a hard failure rather than a slow one.
- The serve log says "rejected unauthorized peer …" and the push fails.
# on the SENDER relayium id # 74318e3b… # on the RECEIVER relayium authorize 74318e3b…serve had no terminal to ask on — a systemd unit or a pipe — so an unknown fingerprint is refused rather than trusted. Pre-authorize it: the fingerprint in the rejection line is exactly the one relayium id prints on the sender, and authorize is idempotent.
- "fingerprint mismatch for receiver.example.com:9031".
grep receiver.example.com ~/.config/relayium/known_hostsThe listener presented a different key than the one pinned on first contact. If you rotated that key on purpose, delete the matching known_hosts line and push again. If you did not, leave the line alone and find out why the key changed before sending anything.
- The systemd unit dies at startup with an insecure-permissions error.
systemctl status relayium-serve # secure: /etc/relayium/id.key has insecure permissions 0644; run: chmod 600 /etc/relayium/id.key ls -l /etc/relayium/id.keyrelayium refuses to load a private key that anyone but its owner can read, the same rule ssh applies. Run chmod 600 on the path the error names, make sure it is owned by the service user, and restart the unit.
Frequently asked questions
How is daemon direct different from push over SSH?
push over SSH tunnels the transfer through your SSH connection and needs an SSH account on the remote. Daemon direct needs no SSH and no account — the two servers authenticate each other by certificate fingerprint over pinned TLS, which is lighter when both machines are yours.
Do I have to copy fingerprints around by hand?
No. In a terminal, serve prompts you to approve each new pusher on its first push — showing its address and fingerprint — and remembers it, so later pushes are silent. You only reach for relayium id or relayium authorize for non-interactive setups like a systemd service, where there's no one to answer the prompt.
Where are the identity and trust files?
In ~/.config/relayium/ by default (override with --config-dir). id.key / id.crt are this host's persistent identity, known_hosts holds fingerprints of listeners you've pushed to, and authorized_fingerprints is the listener's allow-list of pushers.
What happens if a fingerprint changes?
The push refuses and warns. The listener's key is pinned in known_hosts on first use, so a later change — a re-keyed host, or a man-in-the-middle — is rejected rather than silently accepted. Remove the known_hosts line only if you intentionally rotated the key.
Is there any relay fallback?
No. Daemon direct assumes a reachable listener address; if the connection can't be made, it fails. Nothing is ever proxied through Relayium — that's the point of this mode.
Wire up two of your own servers for direct transfers — no relay, no SSH, no pairing code.
Get the CLI