Skip to content

Self-Hosting SimpleX Servers (SMP + XFTP)

SimpleX Chat has no central servers and no user identifiers. Messages and files flow through relays that anyone can run. There are two kinds:

  • SMP server (Simple Messaging Protocol) — carries messages via anonymous queues and holds pending messages temporarily. Default port 5223 (recent clients also try 443).
  • XFTP server (the file-transfer protocol) — stores encrypted file chunks that auto-expire. Default port 443.

By default the app uses SimpleX’s preset operators (you’ll see SimpleX Chat and Flux under Network & servers → Preset servers). Running your own relays gives a community independence from those presets, keeps metadata on infrastructure you control, and survives upstream outages.

This guide covers:

  1. Does self-hosting keep traffic on my servers? — the routing model.
  2. Use a community server in your client — point your app at an existing relay.
  3. Self-host your own relays — stand up SMP + XFTP.
  4. What can the operator see? and a verification checklist.

Does self-hosting keep traffic on my servers?

Section titled “Does self-hosting keep traffic on my servers?”

Yes — if both people’s connection was created using only your relays, the message and file content flows only through your servers. There is no central SimpleX server it has to pass through.

Why: there’s no “network” in the middle. Each user picks which server receives their messages.

  • Your inbound messages land in a queue on your chosen SMP server.
  • Your contact’s inbound messages land in a queue on their chosen SMP server.
  • When you send, your client delivers to the recipient’s receiving queue.

So if both people configure your SMP server to receive, both queues live on your relay and every message between them stays on it. Files work the same way: the sender uploads chunks to the sender’s XFTP server and the recipient downloads from there — both on your XFTP means files stay on your relay too.

To actually achieve “only my servers”:

  1. Both devices add your SMP + XFTP and enable use for new connections and use to receive.
  2. Disable the preset operators — otherwise new queues (and the private-routing hop) may pick a preset server.
  3. Move existing receiving addresses manually or create new connections. New contacts use the new selection automatically; an existing contact can be migrated with Change receiving address, one connection at a time.

A SimpleX server address looks like this:

smp://<fingerprint>:<password>@smp.example.org
xftp://<fingerprint>:<password>@xftp.example.org

The :<password> part is present only on password-protected relays (most community relays are). The address — password included — is what lets you create queues / upload files. Treat the full address as a shared secret.

  1. Open Settings → Network & servers.
  2. Under Messages and files, tap Your servers.
  3. Tap Add serverEnter server manually (or Scan server QR code if your admin shared a QR).
  4. Paste the smp://… address. Repeat Add server for the xftp://… address. SimpleX files each into the right list automatically.
  5. Open each server you added and set how it’s used:
    • Use for new connections — new contacts/groups get created on it.
    • Use to receive — the app accepts messages/files routed through it.
  6. (Optional) To use only your community relays, open the Preset servers (SimpleX Chat, Flux) and toggle them off — but keep at least one enabled server that can receive, or you’ll stop getting messages.
  7. Tap Test servers to verify the app can reach them, then save.

  • A host with one or two public IPs and these free public TCP ports: 5223 (SMP) and 443 (both SMP-on-443 and XFTP want 443 — see below).
  • Docker + Docker Compose.
  • DNS records pointing at your IP(s), DNS-only / not proxied.
  • A little disk for XFTP chunks (transient, auto-expire; a quota caps it).

Recent SimpleX clients connect to SMP on port 443 by default, falling back to 5223. XFTP’s native port is also 443. Two 443 services can’t share one IP, so you have a choice:

LayoutSMPXFTPTrade-off
Two IPs (recommended)IP-A:5223 + IP-A:443IP-B:443Both reachable on 443; needs a 2nd public IP
One IP (simplest)IP:5223 onlyIP:443Official layout, but clients on networks that block 5223 can’t reach SMP

The docker-compose.yml below shows the two-IP layout. For one IP, drop the second SMP port line and point both DNS records at the same IP.

name: simplex-server
services:
smp-server:
image: simplexchat/smp-server:v6.5.2 # example tested pin; review upstream releases
container_name: simplex-smp-server
restart: unless-stopped
environment:
ADDR: smp.example.org # your SMP FQDN
PASS: ${SMP_PASSWORD} # queue-creation password
WEB_MANUAL: "1" # see gotcha below — load-bearing
volumes:
- ./data/smp/config:/etc/opt/simplex # CA key + fingerprint (BACK UP)
- ./data/smp/state:/var/opt/simplex
ports:
- "203.0.113.10:5223:5223" # bind ONE public IP, not 0.0.0.0
- "203.0.113.10:443:443" # SMP also on 443 (drop this for one-IP setups)
xftp-server:
image: simplexchat/xftp-server:v6.5.2 # example tested pin; review upstream releases
container_name: simplex-xftp-server
restart: unless-stopped
environment:
ADDR: xftp.example.org
QUOTA: 100gb # refuse uploads past this
PASS: ${XFTP_PASSWORD}
volumes:
- ./data/xftp/config:/etc/opt/simplex-xftp # fingerprint (BACK UP)
- ./data/xftp/state:/var/opt/simplex-xftp
- ./data/xftp/files:/srv/xftp # encrypted chunks (QUOTA applies)
ports:
- "203.0.113.11:443:443" # XFTP on its OWN IP's 443

Set a strong password once (it goes in the client address, so keep it URL-safe):

Terminal window
printf 'SMP_PASSWORD=%s\nXFTP_PASSWORD=%s\n' \
"$(openssl rand -hex 24)" "$(openssl rand -hex 24)" > .env
docker compose up -d

On first start each server generates its CA, certificate, and fingerprint:

Terminal window
cat data/smp/config/fingerprint
cat data/xftp/config/fingerprint

Assemble the addresses you hand to clients (note the :PASS):

smp://<smp-fingerprint>:<SIMPLEX_PASSWORD>@smp.example.org
xftp://<xftp-fingerprint>:<SIMPLEX_PASSWORD>@xftp.example.org

Create A records for each relay, DNS-only / not proxied (grey cloud in Cloudflare — orange breaks it):

NameTypeValueProxy
smp.example.orgA203.0.113.10DNS only
xftp.example.orgA203.0.113.11DNS only

(One-IP setup: both point at the same address.)

Publishing a Docker port is not always enough. On hosts where Docker’s DOCKER-USER chain ends in a default-deny (a common hardening pattern), the relay listens but the internet can’t reach it — you must explicitly allow the pre-DNAT public IP:port, before the DROP:

Terminal window
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 5223 -j ACCEPT
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdst 203.0.113.10 --ctorigdstport 443 -j ACCEPT # SMP-on-443
iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdst 203.0.113.11 --ctorigdstport 443 -j ACCEPT # XFTP
netfilter-persistent save # persist across reboots

On a plain ufw/firewalld host without a default-deny DOCKER-USER chain, just open the ports. Re-apply after any Docker reinstall — Docker rebuilds DOCKER-USER and drops your rules until they’re restored.


Less than a conventional messaging server, but not nothing. A live relay inspection showed the following current behavior; treat it as an observation, not a promise that future versions or added host logging cannot change it:

  • In the inspected application logs/store: no source IPs, user identities, or plaintext content. Store records contained opaque queue/file IDs and protocol material (CREATE/SECURE/FNEW records).
  • At the network layer you can observe the source IP of a connection with tcpdump/ss on the host — but it isn’t written to any log, and a client using Tor, a SOCKS proxy, or private routing shows you only a relay/proxy IP, not the device.
  • The protocol records do not directly identify a user, reveal plaintext, or encode a social graph. A host or network operator can still observe source IP, timing, connection duration, and traffic size and may correlate those signals.

If you add connection-IP logging for abuse handling, document the retention and privacy impact; host-level firewall or flow logging changes the observation above.


Run these after standing a relay up (and keep the list in your runbook). Your own machine often can’t test the public path if it reaches the host over a VPN/tailnet — test from a true external vantage.

Reachability & identity

Terminal window
# TCP reachable from off-site (or use https://check-host.net/check-tcp):
bash -c '</dev/tcp/smp.example.org/5223 && echo SMP-5223-OK'
bash -c '</dev/tcp/smp.example.org/443 && echo SMP-443-OK'
bash -c '</dev/tcp/xftp.example.org/443 && echo XFTP-OK'
# the CA fingerprint the server SERVES must equal the one in your address
# (detects MITM / wrong address):
echo | openssl s_client -connect smp.example.org:443 -servername smp.example.org -showcerts 2>/dev/null \
| awk '/BEGIN/{n++} n==2' | openssl x509 -outform DER | openssl dgst -sha256 -binary | openssl base64

Functional (in a client)

  • Add both servers, disable presets, and make a brand-new contact between two devices — proves your relays alone carry traffic. Confirm on the host: wc -l data/smp/state/smp-server-store.log grows (CREATE/SECURE).
  • Send a multi-MB file (small files/images go inline over SMP, so they never touch XFTP). Confirm FNEW/FPUT appear in data/xftp/state/file-server-store.log.
  • Add the SMP address once plain (defaults to 443) and once with explicit :5223 — both should pass Test servers.
  • Test from a restrictive network (cellular / corporate) to confirm 443 works.

Password / security

  • Add the server address without the :password@ part → creating a queue must fail (proves it’s not an open relay).
  • ss -tlnp — confirm the relays bind only the intended public IP(s), not 0.0.0.0.

Resilience

  • docker compose restart → fingerprint unchanged, queues restored.
  • Reboot the host → firewall rules reload (netfilter-persistent) and relays return.
  • Backup/restore drill (most important): back up data/{smp,xftp}/config/, restore into a fresh container, confirm the same fingerprint comes up. Losing the CA key = losing the server identity = every client breaks.

  • Back up data/{smp,xftp}/config/ off-box — CA private key + fingerprint = the server identity.
  • Quota: QUOTA caps stored XFTP chunks; files auto-expire (~48 h by default, configurable). Raise any time and docker compose up -d.
  • Cert rotation: rotate the TLS cert ~every 3 months with the offline CA key (smp-server cert); the fingerprint/identity is preserved, so addresses don’t change.
  • Password vs open: setting PASS restricts queue/file creation to people you give the address to. Omit it only if you intend a public-good relay.
  • WAN dependency: a relay on a public IP is unreachable while that WAN link is down — unlike HTTP services, it can’t fall back through a tunnel.
  • Updates: use explicit tested version tags or digests, record the prior references for rollback, then pull and recreate deliberately.