Running Multiple Services
One HiddenForge container can host several independent .onion addresses at once: a public site, a private admin panel, a file drop, each with its own address and its own key. This page shows how.
The idea
Every environment variable ending in _TOR_SERVICE_HOSTS becomes its own hidden service, with its own .onion. Add more variables, get more services. Each one is independent: knowing one address tells an outsider nothing about the others.
Example: a public site and a private panel
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
- 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
pids_limit: 256
ulimits:
nofile: {soft: 8192, hard: 8192}
nproc: {soft: 256, hard: 256}
environment:
BLOG_TOR_SERVICE_HOSTS: "80:blog:80"
ADMIN_TOR_SERVICE_HOSTS: "80:admin:8080"
volumes:
- ./tor-keys:/var/lib/tor/hidden_service
networks: [tor-internal, tor-external]
depends_on: [blog, admin]
blog:
image: nginx:alpine
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]
admin:
image: your-admin-app:latest
read_only: true
networks: [tor-internal]
networks:
tor-internal:
internal: true
tor-external: {}
This gives you two separate .onion addresses. Read the folder names to get each one:
docker compose exec tor cat /var/lib/tor/hidden_service/blog/hostname
docker compose exec tor cat /var/lib/tor/hidden_service/admin/hostname
The folder name comes from the variable prefix, lowercased with underscores turned into hyphens. BLOG becomes blog, and MY_ADMIN would become my-admin.
Multiple ports on one service
You can also expose more than one port for a single service by separating mappings with commas:
WEB_TOR_SERVICE_HOSTS: "80:web:80,443:web:443"
Both ports belong to the same .onion address.
Reading the mapping format
Each mapping is onion_port:backend_name:backend_port. There is a two-part shorthand, onion_port:backend_port, but it points at 127.0.0.1 inside the tor container itself, which is only useful if your app runs in the same container. That is not the normal setup here. For the standard "separate backend container" layout, always use the three-part form with the backend's service name.
If one mapping is wrong
HiddenForge is forgiving about mistakes. A single bad mapping (an invalid port, an unreachable backend name) is skipped with a warning in the logs, and the rest of your services still come up. An invalid service name skips just that one variable. The container keeps running either way, so if a service is missing, check the logs for a Skipping line rather than assuming the whole container failed.
Keep the security settings on every backend
Each backend you add should follow the same pattern as the example: on tor-internal only, no ports: mapping, read_only with small noexec,nosuid,nodev tmpfs mounts for anything it must write. A backend with a published port or an internet route is the one mistake that undoes the whole design. See OPSEC: Mistakes That Deanonymize You.
HiddenForge
Start here
Going deeper
- The Security Model
- Verifying the Image
- Backing Up Your Onion Address
- Upgrading Safely
- Running Multiple Services
Help