Tor Hidden Service Docker
A lightweight, secure Docker container for running Tor hidden services. Built on Alpine Linux with a focus on minimal footprint and maximum security.
- π Quick Start
- β¨ Features
- βοΈ How It Works
- π¦ Usage
- π§ Configuration
- π‘οΈ Security
- π Key Persistence
- π Examples
- π Health Checks
- π Supply Chain
- π Troubleshooting
- π€ Contributing
# Run with a simple web service
docker run -d --name my-tor-service \
-e HS_WEB=web:80:80 \
ghcr.io/hundehausen/tor-hidden-service:latest
# View your .onion address
docker logs my-tor-serviceOr use Docker Compose:
git clone https://github.com/hundehausen/tor-hidden-service-docker.git
cd tor-hidden-service-docker
docker compose up -d
docker compose logs -f tor- πͺΆ Minimal footprint - Alpine Linux base (~44MB)
- ποΈ Easy hidden services - Configure via environment variables
- π Security-first - Runs as non-root user with minimal privileges
- π‘οΈ Input validation - Defense-in-depth against path traversal and injection
- π Auto-discovery - Onion addresses displayed in logs
- π Key persistence - Reuse existing keys across restarts
- π Health checks - Built-in container health monitoring
- π Graceful shutdown - Proper SIGTERM/SIGINT handling with 30s timeout
- ποΈ Multi-arch - Supports linux/amd64 and linux/arm64
- The container runs a Tor daemon that registers your services on the Tor network
- Each
HS_*environment variable creates a hidden service with its own.onionaddress - Incoming connections to a
.onionaddress are forwarded to the specified target container and port - A SOCKS proxy on port 9050 is available for outbound Tor connections
docker run -d --name tor-hidden-service \
-e HS_WEB=web-container:80:80 \
ghcr.io/hundehausen/tor-hidden-service:latestservices:
web:
image: nginx:alpine
container_name: web-container
volumes:
- ./example-site:/usr/share/nginx/html
networks:
- tor-network
tor:
image: ghcr.io/hundehausen/tor-hidden-service:latest
container_name: tor-hidden-service
environment:
- HS_WEB=web:80:80
- SOCKS_BIND=127.0.0.1
volumes:
- tor-data:/var/lib/tor
networks:
- tor-network
deploy:
resources:
limits:
memory: 256M
cpus: '0.5'
networks:
tor-network:
driver: bridge
volumes:
tor-data:From container logs:
docker logs tor-hidden-serviceOutput:
======== TOR HIDDEN SERVICES ========
WEB: 2xxiyj6noereyty4xdxjg5akcop7ylnotvf4fqre57g7xuppy4tixvqd.onion
API: 7gtpkyhhhsbowew7zha6z7h7vlffqkn3nbu2f6hgin4xddq22o7yytyd.onion
====================================
From the filesystem:
# Get specific service address
docker exec tor-hidden-service cat /var/lib/tor/WEB/hostname
# List all services
docker exec tor-hidden-service ls /var/lib/tor/Configure hidden services using the format:
HS_[SERVICE_NAME]=[TARGET_HOST]:[TARGET_PORT]:[VIRTUAL_PORT]
| Variable | Description | Example |
|---|---|---|
SERVICE_NAME |
Unique identifier (alphanumeric, hyphens, underscores) | WEB, API, BLOG |
TARGET_HOST |
Hostname or container name | web, api.internal |
TARGET_PORT |
Port the service listens on | 80, 8080 |
VIRTUAL_PORT |
(Optional) Port exposed on .onion address, defaults to TARGET_PORT |
80 |
Examples:
# Single service
HS_WEB=web-container:80:80
# Multiple services (each gets a unique .onion address)
HS_WEB=web:80:80
HS_API=api:8080:80
HS_BLOG=blog:3000:80The container exposes a SOCKS5 proxy on port 9050 for routing traffic through Tor.
By default, the proxy binds to 0.0.0.0 (all interfaces). For production, restrict this to localhost:
environment:
- SOCKS_BIND=127.0.0.1Note: The default will change to
127.0.0.1in a future major version.
You can also pass services as arguments:
docker run -d \
ghcr.io/hundehausen/tor-hidden-service:latest \
web:web-container:80:80 \
api:api-container:8080:80Two env vars accept multi-line torrc content, appended to /etc/tor/torrc at boot:
| Variable | Where it lands | Use for |
|---|---|---|
TORRC |
After SocksPort, before any hidden service section |
Global directives |
HSTORRC_<SERVICE> |
After that service's HiddenServicePort line |
Per-service directives (PoW, IntroDoS, MaxStreams, β¦) |
Hyphens in service names become underscores (HS_MY-API β HSTORRC_MY_API).
Tor validates directives at startup; bad config fails fast in the logs.
services:
tor:
image: ghcr.io/hundehausen/tor-hidden-service:latest
environment:
SOCKS_BIND: 127.0.0.1
HS_WEB: web:80:80
HS_API: api:8080:80
TORRC: |
ClientUseIPv6 1
HSTORRC_WEB: |
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 250
HiddenServicePoWQueueBurst 1000
HiddenServiceEnableIntroDoSDefense 1
HiddenServiceEnableIntroDoSRatePerSec 25
HiddenServiceEnableIntroDoSBurstPerSec 200
HSTORRC_API: |
HiddenServiceMaxStreams 50
HiddenServiceMaxStreamsCloseCircuit 1Inspect the rendered config:
docker exec tor-hidden-service cat /etc/tor/torrc- π Set resource limits - Prevent resource exhaustion (see Usage)
- π Restrict SOCKS proxy - Set
SOCKS_BIND=127.0.0.1in production - πΎ Persist keys securely - Mount
/var/lib/toras a volume, never commit keys to git
The hs_ed25519_secret_key files in /var/lib/tor/[service]/ are the cryptographic identity of your .onion address. If compromised, an attacker can impersonate your service.
The entrypoint script validates all inputs:
- Service names - Alphanumeric, hyphens, and underscores only (max 64 chars)
- Ports - Numeric, range 1-65535
- Hostnames - No shell metacharacters allowed
- Path traversal - Prevented via allowlist and path verification
volumes:
- tor-data:/var/lib/torvolumes:
- tor-data:/var/lib/tor
- ./backup-keys/WEB:/var/lib/tor/WEB # Restore specific service
- ./backup-keys/API:/var/lib/tor/API# Copy keys from running container
docker cp tor-hidden-service:/var/lib/tor/WEB ./backup-keys/
# Or archive the entire volume
docker run --rm -v tor-data:/data -v $(pwd):/backup alpine \
tar czf /backup/tor-keys.tar.gz -C /data .services:
nginx:
image: nginx:alpine
volumes:
- ./website:/usr/share/nginx/html:ro
networks:
- tor
tor:
image: ghcr.io/hundehausen/tor-hidden-service:latest
environment:
- HS_SITE=nginx:80:80
- SOCKS_BIND=127.0.0.1
volumes:
- tor-keys:/var/lib/tor
networks:
- tor
networks:
tor:
volumes:
tor-keys:services:
web:
image: nginx:alpine
networks:
- tor
api:
image: node:24-alpine
networks:
- tor
tor:
image: ghcr.io/hundehausen/tor-hidden-service:latest
environment:
- HS_WEBSITE=web:80:80
- HS_API=api:3000:80
- HS_ADMIN=api:8080:80
- SOCKS_BIND=127.0.0.1
volumes:
- tor-data:/var/lib/tor
networks:
- tor
networks:
tor:
volumes:
tor-data:The container includes a health check that verifies Tor network connectivity:
- Interval: 300 seconds (5 minutes)
- Timeout: 3 seconds
- Test: Connects to
check.torproject.orgvia SOCKS proxy
Check health status:
docker ps --filter name=tor-hidden-service --format "table {{.Names}}\t{{.Status}}"Every published image on ghcr.io/hundehausen/tor-hidden-service ships with three attestations:
- SBOM (Software Bill of Materials, SPDX 2.3) β every package inside the image
- Provenance (SLSA v0.2) β how the image was built, from which commit, by which CI
- Sigstore signature β keyless, signed via GitHub OIDC, recorded in Rekor
Requires cosign:
cosign verify ghcr.io/hundehausen/tor-hidden-service:latest \
--certificate-identity-regexp 'https://github\.com/hundehausen/tor-hidden-service-docker/\.github/workflows/.+' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'Exit code 0 and a JSON payload listing one signature means the image was built by this repo's CI β not a third-party reupload.
docker buildx imagetools inspect ghcr.io/hundehausen/tor-hidden-service:latest \
--format '{{ json (index .SBOM "linux/amd64").SPDX }}'Returns SPDX JSON listing every Alpine package and version inside the image.
docker buildx imagetools inspect ghcr.io/hundehausen/tor-hidden-service:latest \
--format '{{ json .Provenance }}'Returns SLSA v0.2 JSON with the source repo URL, commit SHA, builder version, and platform. Confirms the image came from this repo's main branch at a specific commit.
Can't connect to hidden service
- Verify Tor bootstrap:
docker logs tor-hidden-service | grep "Bootstrapped 100%" - Check service configuration:
docker exec tor-hidden-service cat /etc/tor/torrc - Ensure target container is reachable:
docker exec tor-hidden-service ping web
Keys are not being persisted. Add a volume for /var/lib/tor:
volumes:
- tor-data:/var/lib/torSet resource limits in your compose file:
deploy:
resources:
limits:
memory: 256MContributions are welcome! Please open an issue or submit a pull request on GitHub.
Disclaimer: This tool is for legitimate privacy purposes only. Users are responsible for complying with all applicable laws and regulations in their jurisdiction.