Server-to-server transfers with the Relayium CLI (daemon direct)
Last updated: 2026-09-01
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 ./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…)- serve handles connections concurrently, up to 64 at once, and lands files under --dir.
- --dir must already exist and be writable by the user serve runs as. serve checks that before it binds the port, so a mistyped path fails immediately instead of becoming a listener that cannot write.
- The default port is 9031; change it with --port and open it on your firewall.
- With no --bind, serve listens on every interface, including public ones, and relies on your firewall. Pass --bind 10.0.0.5 (or --bind 127.0.0.1 behind a tunnel) to limit the listener itself.
- Being logged in to a Relayium account has nothing to do with this. An account grants no one filesystem access to this host: a push is accepted only when the pusher's fingerprint is in this listener's own authorized_fingerprints.
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: each file is checked with a per-file SHA-256 and staged before it is installed. push does not resume, here or over SSH — it refuses a destination that already exists, so an interrupted run is finished by pushing the missing paths, or by relayium sync, which continues a partial file and which this listener honours unless it was started with --no-resume.
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 a deploy script or CI for encrypted, integrity-checked server-to-server delivery. For a job that runs on a schedule into the same directory, use relayium sync instead: push refuses a destination that already exists, so a repeated push into one fixed path succeeds once and is refused after that. 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 --config-dir /etc/relayium 74318e3b...
- Identity and trust files live in ~/.config/relayium/ (override with --config-dir, e.g. /etc/relayium for a service).
- Give authorize the same --config-dir the listener runs with, or it writes a file the listener never reads. A service started with --config-dir /etc/relayium is served by relayium authorize --config-dir /etc/relayium <fingerprint>; a bare authorize would write ~/.config/relayium/authorized_fingerprints instead, and the push would keep being rejected.
- A running listener picks up a newly authorized fingerprint on its next connection — you do not restart it. Revocation is not symmetric: to withdraw access, remove the line and restart the listener.
- authorize is idempotent — running it again for the same fingerprint is a no-op.
- To mirror a folder rather than copy into it, use relayium sync with the same relayium:// destination. push never overwrites — a name that already exists on the receiver is refused, and the refusal names the path — while sync sends only what changed and can mirror deletions.
- sync --delete removes files the source dropped, but only when the listener was started with --allow-delete; otherwise nothing is deleted and the sender is told so. Deletion is confined to the top-level directories that transfer actually sends: mirroring ./site touches only site/ under the receiver's --dir, a sibling folder or unrelated file is never removed, empty directories are pruned only inside those same roots, and two sources mirrored at once cannot delete each other's files. A single-file source scopes only that file, so it deletes nothing; an empty or unresolvable source refuses the delete outright on both ends, because it would otherwise mean "delete everything".
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.
- Create /srv/inbox first and make it writable by the service user. serve validates the directory before it binds, so the unit fails at startup with a clear message rather than accepting a transfer it cannot land.
- Authorize new pushers against the SAME directory: relayium authorize --config-dir /etc/relayium <fingerprint>. No systemctl restart is needed — the running listener reads the file again on the next unknown fingerprint.
- Keep /etc/relayium/id.key readable only by the service user — relayium refuses to load a key with loose permissions.
- Add --bind to the ExecStart line if the unit should listen on one address only.
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 --config-dir /etc/relayium 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 and push again: the listener re-reads its allow-list whenever it meets a fingerprint it does not know, so this takes effect on the next connection, with no restart. The fingerprint in the rejection line is exactly the one relayium id prints on the sender, and authorize is idempotent. If it still fails, add the listener's own --config-dir to the authorize command — writing the fingerprint into a different directory than the one serve reads is the usual cause.
- "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.
Do I need a Relayium account, and does logging in give another machine access?
No, and no. Daemon direct never contacts Relayium, so no account is involved on either side. Logging in also grants nobody filesystem access: a listener accepts a pusher only when that pusher's fingerprint is in its own authorized_fingerprints file. Account login and listener authorization are separate decisions, and neither implies the other.
Should I use Device Inbox for this instead?
No — the CLI's Inbox is the receiving side only. It accepts files your account sends to that machine, and there is no CLI command that sends into an inbox; you send to one from the Web app or a native app. To move files between two servers you control, serve plus push or sync is the direct path, and it needs no account at all.
Can a push delete files on the receiver?
A plain push never deletes and never overwrites — a collision is refused with the offending path named. sync --delete can remove files, but only if the listener was started with --allow-delete, and only inside the top-level directories that transfer actually sends. Anything else under the receiver's --dir is out of scope, and an empty or unresolvable source refuses the delete outright.
Wire up two of your own servers for direct transfers — no relay, no SSH, no pairing code.
Get the CLI