Relayium

Self-host Relayium: run your own file-and-text transfer server

Last updated: 2026-08-06

Relayium is AGPL-3.0-licensed and open source, and the server is one self-contained image — no external database, no third-party storage bucket, nothing to sign up for. If you'd rather run the whole thing yourself than rely on relayium.com, this guide gets a server up with Docker and points the CLI at it.

Self-hosting gives you full control over where your data lives, your own domain and TLS certificate, and no dependency on anyone else's infrastructure. Everything below is grounded in the files that ship in the repo — docker-compose.yml, server/.env.example, and docs/self-hosting.md — so nothing here is a flag or setting that doesn't actually exist.

Why self-host

Relayium's realtime transfers are end-to-end encrypted. A self-hosted TURN relay may carry ciphertext bytes, and the server handles signaling metadata, but neither can read or decrypt file plaintext; neither the server nor relay stores a server-side copy or history of realtime content. The server does hold your account and — for stored/link-based transfers — ciphertext blobs and a small SQLite database. Self-hosting means that data lives on infrastructure you control, under your own domain, with nobody else's operational decisions in the loop.

Because the project is AGPL-3.0-licensed and open source (github.com/relayium/relayium), you can read exactly what the server does before you trust it with anything, and fork or modify it freely.

Quick start with Docker

What you need before step 1

  • A host with Docker Engine and the Compose plugin. docker compose version prints a version string; "docker: 'compose' is not a docker command" means the plugin is missing.
  • A clone of the repository. The compose file builds the image from this source tree, so it needs the Dockerfile and web/ beside it — there is no prebuilt image to pull.
  • Disk for the named volume relayium-data: the SQLite database plus any stored-transfer ciphertext you keep.
  • A domain and a TLS-terminating reverse proxy if anyone but you will use it. The container speaks plain HTTP and publishes on the loopback interface only.
  • Nothing else. No external database, no object-storage bucket, no third-party account.

The repo root ships a Dockerfile and a docker-compose.yml that build one self-contained image — a static Go binary that serves the prebuilt web app, so there is no separate Node, Go toolchain, or nginx required just to run it.

  1. Clone the repository and change into it.

    git clone https://github.com/relayium/relayium.git
    cd relayium
  2. Build and start it. The placeholder secret is required even though the relay is off: Compose validates the gated coturn service's required variable while it parses the file, so a bare docker compose up refuses to start.

    RELAYIUM_TURN_SECRET=placeholder docker compose up -d --build
  3. Confirm the container stayed up rather than crash-looping.

    docker compose ps
  4. Ask the instance whether it can actually serve. Use /readyz, not /healthz — the difference between them is the whole point of the check, and the expected-result box below spells it out.

    curl -s http://127.0.0.1:8080/readyz
  5. Copy the config template and set your public URL. RELAYIUM_BASE_URL builds the links in outgoing email and decides whether session cookies carry the Secure flag, so it has to be your real https:// address.

    cp server/.env.example server/.env
    chmod 600 server/.env
  6. Put nginx or Caddy in front, terminating TLS for your domain and proxying everything — /, /api, /ws, /admin — to port 8080. Then restart to pick up server/.env.

    docker compose up -d

What a working instance looks like

The container reports Up, and both endpoints answer. ready is the one that matters: /healthz returns ok unconditionally, before anything is opened, so it also passes on an instance whose database or blob directory is unusable. /readyz pings the SQLite database and the blob directory and answers 503 when either is broken.

$ docker compose ps
NAME                IMAGE                     STATUS          PORTS
relayium-server-1   relayium/relayium:local   Up 12 seconds   127.0.0.1:8080->8080/tcp

$ curl -s http://127.0.0.1:8080/healthz
ok
$ curl -s http://127.0.0.1:8080/readyz
ready

Add a TURN relay for cross-network transfers

Same-network (LAN) transfers and SSH-based push/pull work with nothing extra. Cross-network realtime transfers (two devices behind different NATs) sometimes need a TURN relay to establish a path — the relay only ever sees ciphertext, never your file contents.

docker-compose.yml has an optional relay profile that starts coturn (the TURN server) and a small Redis instance for relay-byte metering, alongside the main server.

The secret has to reach two different places, and getting that wrong fails silently. coturn receives it through Compose variable interpolation, which resolves from the shell or a project-root .env. The server reads it from its own environment, which means server/.env — and an empty secret disables TURN outright. Set only one of them and you get a running coturn the server never issues credentials for: every container reports healthy, nothing is logged, and strict-NAT transfers keep failing exactly as they did before.

  1. Generate one long random secret. Use the same value everywhere below.

    openssl rand -hex 32
  2. Put that secret, and the relay addresses your domain resolves to, in server/.env so the server enables TURN at all.

    RELAYIUM_TURN_SECRET=<the value from step 1>
    RELAYIUM_TURN_URLS=turn:example.com:3478,turns:example.com:5349
  3. Export that same file into the shell so Compose's interpolation can hand coturn the identical secret. Sourcing it keeps one source of truth and keeps the secret off the command line, where ps would expose it.

    set -a; . ./server/.env; set +a
  4. Start the stack with the relay profile.

    docker compose --profile relay up -d --build
  5. Open the relay ports on the host firewall. coturn runs with host networking, so these are host rules, not Docker ones: UDP 3478 and 49152-65535, TCP 3478 and 5349.

  6. Confirm the server — not just coturn — came up holding the secret. This is the check that catches the silent case.

    docker compose exec server env | grep RELAYIUM_TURN

What a working relay looks like

Both keys come back non-empty from inside the server container. coturn being up tells you nothing on its own — the browser only ever gets relay credentials the server minted.

$ docker compose exec server env | grep RELAYIUM_TURN
RELAYIUM_TURN_SECRET=3f7a…
RELAYIUM_TURN_URLS=turn:example.com:3478,turns:example.com:5349

Install the CLI on your machine

This last step runs the relayium CLI on your own computer (not the server), so install it there if you haven't. On macOS or Linux:

curl -fsSL https://relayium.com/install.sh | sh

Point the CLI at your server

The Relayium CLI defaults to relayium.com's rendezvous server for cross-network send/receive and text. Pass --server to use your own instead.

  1. Sign in against your server rather than relayium.com, on a machine that already has the CLI from the section above. It prints a URL and a code; approve it in a browser signed in to your instance.

    relayium login --server https://your-domain
  2. Confirm which server the stored credentials are bound to. whoami takes no flags — it reports what the login actually wrote, which is what makes it worth running.

    relayium whoami
  3. Send, passing the same --server. Without it the CLI mints the pairing code against relayium.com and the other end will never find it on your instance.

    relayium send ./report.pdf --server https://your-domain
  4. Receive on the other machine with the printed code and the same --server. Text sessions work the same way.

    relayium receive 483920 --server https://your-domain
    relayium text --server https://your-domain
    relayium text 483920 --server https://your-domain

How you know it is talking to your instance

whoami prints the account followed by the server it is bound to, in parentheses. Your own domain there — not relayium.com — is the confirmation.

$ relayium login --server https://your-domain
Open https://your-domain/device and enter code: WDJB-MJHT
logged in as you@example.com

$ relayium whoami
you@example.com (https://your-domain)

When it doesn't work

Five failures cover nearly every unsuccessful self-host. Each has a line you can read or a command you can run that decides it, and three of the five look like success until you run the check.

Symptom, check, fix

docker compose up refuses to start at all, before anything is built.
docker compose up -d --build
# required variable RELAYIUM_TURN_SECRET is missing a value

Compose interpolates the whole file before it filters by profile, so the gated coturn service's required variable is validated even though the relay is off. Prefix any placeholder — RELAYIUM_TURN_SECRET=placeholder docker compose up -d --build — and replace it with a real secret only when you actually enable the relay profile.

The container is Up, but a browser on another machine cannot reach it.
docker compose ps
# PORTS  127.0.0.1:8080->8080/tcp

That loopback bind is the default, so a public host does not expose plaintext HTTP to the internet. For production, leave it and terminate TLS in a reverse proxy on the same host. For a LAN-only box with no proxy, publish it wider with RELAYIUM_BIND=0.0.0.0 docker compose up -d — that variable is read by compose, not by the server.

/healthz says ok, but sign-up fails and stored links never appear.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/readyz
# 503

/healthz answers ok unconditionally and proves only that the process is listening. /readyz pings the SQLite database and the blob directory, so a 503 means one of them is unusable — check that the relayium-data volume is mounted and that RELAYIUM_DB and RELAYIUM_BLOB_DIR point inside it.

relayium login prints a verification URL on localhost that you cannot open.
relayium login --server https://your-domain
# Open http://localhost:8080/device and enter code: WDJB-MJHT

The server builds that URL from RELAYIUM_BASE_URL, which defaults to http://localhost:8080. Set it in server/.env to your real https:// address and restart. It also decides whether session cookies carry the Secure flag, so leaving it wrong is not only cosmetic.

coturn is running, but cross-network transfers through strict NAT still fail — with nothing logged.
docker compose exec server env | grep RELAYIUM_TURN
# (no output)

The secret reached coturn through Compose interpolation but never reached the server, whose empty secret disables TURN outright. Put RELAYIUM_TURN_SECRET and RELAYIUM_TURN_URLS in server/.env, source it with set -a; . ./server/.env; set +a so interpolation sees the same value, and restart the relay profile. Both keys must come back non-empty from that check.

Frequently asked questions

Do I need to set up TURN?

Only if you want cross-network realtime transfers to work through strict NATs. Same-network transfers, SSH-based push/pull, and daemon-direct all work without it — TURN is purely for NAT traversal on the cross-network pairing-code path.

Is the CLI still free if I self-host?

Yes. The CLI is completely free whether it talks to relayium.com or a server you run yourself — --server just points it at your instance. An account on that server is needed by send or text when run without a code to mint one, and by up to store a file. receive, down, and text with a printed code need no login.

Can I use my own domain and TLS certificate?

Yes. The Docker image listens on plain HTTP on :8080; put nginx or Caddy in front with your own domain and certificate (e.g. via certbot/Let's Encrypt). docs/self-hosting.md covers what to proxy; Relayium's own production nginx configuration isn't published, so you'll write your own.

What data does my self-hosted server store?

A SQLite database (accounts, sessions) at RELAYIUM_DB and, for stored/link-based transfers, encrypted blobs at RELAYIUM_BLOB_DIR that the server itself cannot decrypt. The server keeps no realtime file or message body and only relays the signaling handshake; receiving endpoints can still save files or retain text.

Install the free Relayium CLI and point it at your own server with --server.

Get the CLI

Keep reading