Self-hosting
Run your own copy, so you pick the version of the code you're trusting.
Set it up
You need a computer that stays on, such as the one running your node, with Docker installed, and your own Electrum server.
1. Start the wallet
Replace 192.168.1.20:50001 with your Electrum server’s address and port:
curl -O https://raw.githubusercontent.com/unbeholdendev/unbeholden-wallet/main/compose.yaml
ELECTRUM=192.168.1.20:50001 docker compose up -d
Compose starts it again after a restart.
ELECTRUM makes the container pass the wallet’s traffic on to your Electrum server, for servers without WebSocket. If your server has a WebSocket port, you can leave ELECTRUM out and type the server’s address on the connection screen. See Without the container in between.
2. Open it on your devices
On the computer running the container, open http://localhost:8080.
From your phone or another computer, the wallet needs https in front of it. One way is Tailscale, installed on that computer and on each device. Turn on HTTPS certificates in its admin console, then run:
tailscale serve --bg 8080
and open https://<machine>.<tailnet>.ts.net. The command prints the exact address. Another way is your own domain.
3. Connect
With ELECTRUM set, the connection screen fills in your server’s address, so you only need to press Connect.
To update later, run docker compose pull && docker compose up -d.
Other setups
You don’t need any of this if the steps above work for you.
Your own domain
Instead of Tailscale, put Caddy in front, in a Caddyfile:
wallet.example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy gets the certificate itself, which needs port 80 open to the internet or a DNS challenge. Keep the container bound to 127.0.0.1: opened over plain http from another device, the app stops, because the browser doesn’t give a plain http page the encryption the vault needs.
An SSL port
Use ELECTRUM=ssl://host:port.
Without the container in between
To connect straight to your Electrum server, it needs a WebSocket port. Your server’s docs show how to open one.
On http://localhost:8080, plain ws:// works and needs no certificate, for example ws://192.168.1.20:50003.
From another device over https, the browser only opens wss://, with a certificate it trusts on your node: from Let’s Encrypt, tailscale cert, mkcert, or a self-signed one you accept once on each device (the connection screen shows how). In Fulcrum, for example, that goes in fulcrum.conf:
wss = 0.0.0.0:50004
cert = /path/to/node.example.com.crt
key = /path/to/node.example.com.key
Restart the server and connect to wss://node.example.com:50004. Let’s Encrypt and Tailscale certificates last 90 days, so renew them and restart the server before then.
Esplora
Your Esplora must send Access-Control-Allow-Origin naming the app’s origin (electrs: --cors <origin>), or the browser won’t let the app read its replies. Esplora doesn’t announce new blocks, so the app checks for a new block every 30 seconds, and a payment in the mempool shows at the next block or when you refresh.
One version
Set TAG=<version> when you run docker compose. The image is ghcr.io/unbeholdendev/unbeholden-wallet.
Without Compose
Run the image:
docker run --rm -p 127.0.0.1:8080:8080 -e ELECTRUM=192.168.1.20:50001 ghcr.io/unbeholdendev/unbeholden-wallet
Or build it from a checkout you’ve verified:
docker build --build-arg APP_VERSION=$(git describe --tags --always) -t unbeholden-wallet .
docker run --rm -p 127.0.0.1:8080:8080 unbeholden-wallet
Verify a build
Builds are reproducible: the same commit gives the same files, byte for byte. Each release publishes hashes.txt and a tarball of the built files, and CI checks that two builds of every commit match.
git checkout <release tag>
bun install --frozen-lockfile
bun run build
bun run --silent hashes | diff - hashes.txt