Quickstart
This gets you a working .onion address pointing at a web app. The example uses Docker Compose and a plain nginx backend; swap in your own app once it works.
You need Docker (or Podman) with the Compose plugin. Nothing else.
1. Create a folder and a compose file
Make a folder to hold your setup and a place for the keys:
mkdir -p hiddenforge/tor-keys
cd hiddenforge
Save this as docker-compose.yml:
networks:
tor-internal:
internal: true # backend lives here: no internet, no exposure
tor-external: {} # lets the tor container reach the Tor network
services:
tor:
image: doingfedtime/hiddenforge:latest
restart: unless-stopped
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID]
security_opt:
- no-new-privileges:true
- seccomp=unconfined # Tor's own sandbox does the syscall filtering
- apparmor=docker-default
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=64m
- /etc/tor:rw,noexec,nosuid,nodev,size=8m
- /var/lib/tor:rw,noexec,nosuid,nodev,size=64m,uid=100,gid=101,mode=0700
mem_limit: 512m
memswap_limit: 512m # equal to mem_limit means no swap, keys never hit disk
pids_limit: 256
environment:
# onion_port : backend_service_name : backend_port
MYSITE_TOR_SERVICE_HOSTS: "80:web:80"
volumes:
- ./tor-keys:/var/lib/tor/hidden_service # your .onion key lives here
networks: [tor-internal, tor-external]
depends_on: [web]
web:
image: nginx:alpine # replace with your own app
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=32m
- /var/cache/nginx:rw,noexec,nosuid,nodev,size=32m
- /var/run:rw,noexec,nosuid,nodev,size=8m
networks: [tor-internal] # reachable by tor, not from the internet
The image is on Sam's own registry (
sambent.dev/sam/hiddenforge) and mirrored on Docker Hub (doingfedtime/hiddenforge). Either works; use whichever you prefer.
2. Understand the one line you edit
The only required setting is the service-hosts variable:
MYSITE_TOR_SERVICE_HOSTS: "80:web:80"
Read it left to right as onion_port : backend_name : backend_port:
80is the port people connect to on your.onion(80 is normal for a website).webis the name of your backend service in this compose file. HiddenForge looks it up on the internal network and finds its address for you.80is the port your backend actually listens on.
The prefix (MYSITE) becomes your service's folder name, lowercased with underscores turned into hyphens. So MYSITE becomes a service called mysite. You can name it anything.
3. Start it
docker compose up -d
The first start takes a few minutes. Tor has to connect to the network, bootstrap to 100%, build a circuit, and publish your service. Two to three minutes is normal on a first run. The container reports itself "healthy" only once it is genuinely reachable, not just running.
4. Get your address
docker compose exec tor cat /var/lib/tor/hidden_service/mysite/hostname
That prints your .onion. Open it in Tor Browser and you should see your backend.
If it isn't up yet, give it another minute and try again. If it still isn't working after a few minutes, see Troubleshooting.
5. The one thing you must not lose
Your address lives in the ./tor-keys folder you created. Back it up. If you lose the key file inside it, the address is gone for good and there is no way to recover it. Read Backing Up Your Onion Address before you put anything real on it.
Next
- Swap
nginx:alpinefor your own app image, keep it ontor-internalonly, and restart. - Serving more than one site? See Running Multiple Services.
- Want to know what just happened under the hood? See How It Works.
HiddenForge
Start here
Going deeper
- The Security Model
- Verifying the Image
- Backing Up Your Onion Address
- Upgrading Safely
- Running Multiple Services
Help