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

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.