3 Quickstart
Sam Bent edited this page 2026-07-16 14:21:19 -04:00

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:

  • 80 is the port people connect to on your .onion (80 is normal for a website).
  • web is the name of your backend service in this compose file. HiddenForge looks it up on the internal network and finds its address for you.
  • 80 is 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:alpine for your own app image, keep it on tor-internal only, and restart.
  • Serving more than one site? See Running Multiple Services.
  • Want to know what just happened under the hood? See How It Works.