3 min read

SimpleX Without Tears: Wrapped in Onion, Served via Tor

Host your SimpleX SMP server as a Tor hidden service - no DNS, no exposed IP, just pure privacy. This guide shows how to wrap your relay in onion routing using Docker and a custom Tor container. SimpleX without tears, served the anonymous way.

After getting my own SimpleX SMP server running behind Traefik, thanks to my good friend and fellow blogger, I just had to think: "Okay, but why not make it accessible via the darknet?"

Well, now it is.

SimpleX already comes with strong privacy by design. But when you add Tor into the mix, you’re pulling your relay one step further out of the reach of traditional tracking. Hosting your SMP server as a Tor hidden service means: no DNS resolution, no traceable IP, and a juicy .onion address to connect with your most paranoid contacts.

So here's how I set it up. No plugins or mystery scripts. It's just Docker, a custom Tor container, and a few tweaks to make the two talk nicely.


Why Even Bother with Tor?

  • Anonymity: Your public IP is no longer exposed, not even to SimpleX SMP servers.
  • Privacy: All relay connections go over the Tor network.
  • Compatibility: SimpleX can use .onion relay addresses just fine, as long as you're connected to the Tor network.

Also… it’s just kinda cool.


Deployment

docker-compose.yml Add the following snippet to your existing simplex smp server compose file.
  smp_tor:
    build:
      context: .
    container_name: simplex_smp_tor
    restart: unless-stopped
    environment:
      HIDDEN_PORT: 5223
      SERVICE_PORT: 5223
    volumes:
      - $SMP_SERVER_TOR_ONION:/var/lib/tor
      - $SMP_SERVER_TOR_SOCKS:/var/lib/socks
    network_mode: service:smp_server
Dockerfile
FROM debian:bookworm

ENV DEBIAN_FRONTEND=noninteractive

RUN groupadd -g 101 debian-tor && \
  useradd -u 101 -g debian-tor -d /var/lib/tor -s /usr/sbin/nologin -r debian-tor

RUN apt-get update && apt-get install -y sudo curl gnupg lsb-release ca-certificates gettext-base

RUN export CODENAME=$(lsb_release -c | awk '{print $2}') && \
  echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org $CODENAME main" > /etc/apt/sources.list.d/tor.list && \
  echo "deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org $CODENAME main" >> /etc/apt/sources.list.d/tor.list

RUN curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc \
  | gpg --dearmor -o /usr/share/keyrings/tor-archive-keyring.gpg

RUN apt-get update && apt-get install -y tor deb.torproject.org-keyring

RUN mkdir -p /var/lib/tor/onion /var/log/tor && \
  chown -R debian-tor:debian-tor /var/lib/tor /var/log/tor && \
  chmod 700 /var/lib/tor/onion

COPY torrc.template /etc/tor/torrc.template

RUN tor-instance-create socks

RUN mkdir -p /var/lib/socks && \
  chown -R debian-tor:debian-tor /var/lib/socks

COPY torrc-socks /etc/tor/instances/socks/torrc

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh
#!/bin/bash
set -e

# Trap SIGTERM and SIGINT to kill child processes
trap 'kill $(jobs -p)' SIGTERM SIGINT

# Generate torrc with root permissions
envsubst </etc/tor/torrc.template >/etc/tor/torrc

# Change ownership in case it's needed
chown debian-tor:debian-tor /etc/tor/torrc

# Drop to debian-tor and run tor processes
sudo -u debian-tor tor -f /etc/tor/torrc &
sudo -u debian-tor tor -f /etc/tor/instances/socks/torrc &

# Wait for all background jobs
wait
torrc.template
Log notice file /var/log/tor/notices.log
SOCKSPort 0
HiddenServiceNonAnonymousMode 1
HiddenServiceSingleHopMode 1

HiddenServiceDir /var/lib/tor/onion/
HiddenServicePort ${HIDDEN_PORT} localhost:${SERVICE_PORT}
torrc-socks
# Log tor to stdout (since no syslog in container)
Log notice stdout

# Listen for SOCKS proxy
SocksPort 9050

# Use a separate DataDirectory
DataDirectory /var/lib/socks

How It Works

The Tor container runs as a sidecar to your smp_server container by using network_mode: service:smp_server. This lets Tor bind directly to the same localhost interface as the SMP server, so it can expose it as a hidden service. No reverse proxies or extra bridges are needed.

You get a .onion address (saved in your onion volume), and clients connected to the Tor network can use that as a relay address in SimpleX just like they would a normal domain.

It’s also worth noting:

  • The SOCKS proxy is spun up alongside the hidden service so that your clients or bots (if needed) can also route outbound traffic via Tor from the same container.
  • The setup supports volume binding for persistence — your .onion address won’t change unless you wipe the directory.

How to Use the .onion Address

After the container runs once, your .onion address will be available at:

docker exec simplex_smp_tor cat /var/lib/tor/onion/hostname

To use it:

  • Give the .onion address to anyone you want to share your relay with.
  • In SimpleX, they can set it as a relay manually in the app if they have Tor enabled locally, or run their own socks proxy.

Settings -> Network & servers -> Your servers -> Add server -> Enter server manually

smp://<fingerprint>[:<password>]@<public_hostname>,<onion_hostname>:<port>

If you would like more information on hosting your own SimpleX SMP server, please visit the official documentation.