- Dockerfile 57.9%
- Shell 42.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| .gitattributes | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| entrypoint-wrapper.sh | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
Tor Onion Services for Traefik
A lightweight Docker image (Alpine Linux + Tor) that exposes your Traefik-managed services as Tor onion (hidden) services. The image is rebuilt weekly to pick up security updates automatically.
Blog post: Using Tor Onion Services with Traefik
AI Disclosure
This container was originally built by me with some assistance from ChatGPT and then reviewed by Claude AI.
How It Works
Tor Browser ──► Tor Network ──► [ Tor Container ] ──► [ Traefik :8181 ] ──► Backend Service
(onion service) (HTTP, no TLS)
- The Tor container runs a Tor client that registers onion services defined in your
torrc. - Each
HiddenServicePortdirective forwards traffic to a Traefik entrypoint that serves plain HTTP (no TLS redirect), because Tor already provides end-to-end encryption. - Traefik routes the request to the correct backend using standard
Host()rules matching the.onionaddress.
This approach mirrors how Cloudflare Tunnels work, but over the Tor network, with isolated Docker networks keeping services segmented.
Quick Start
1. Create a Docker network for Tor and Traefik
docker network create priv_tor_traefik
2. Add a non-TLS entrypoint to Traefik
Add the following to your Traefik static configuration (command args, TOML, or YAML):
--entrypoints.webnorclear.address=:8181
This entrypoint must not redirect to HTTPS. Tor Browser expects to connect over HTTP to .onion addresses.
3. Create a torrc file
Define one onion service per backend you want to expose:
HiddenServiceDir /var/lib/tor/myapp.example.com/
HiddenServicePort 80 traefik:8181
HiddenServiceDir /var/lib/tor/anotherapp.example.com/
HiddenServicePort 80 traefik:8181
The directory names under /var/lib/tor/ are arbitrary labels (using your clearnet FQDN is a handy convention). All services point at traefik:8181 since Traefik handles the routing.
4. Deploy with Docker Compose
services:
tor:
image: thefizi/alpine-tor-for-traefik:latest
container_name: tor
restart: unless-stopped
volumes:
- ./torrc:/etc/tor/torrc:ro # Your onion service definitions
- tor_keys:/var/lib/tor # Persist onion keys across restarts
networks:
- priv_tor_traefik
traefik:
image: traefik:latest
# ... your existing Traefik configuration ...
networks:
- priv_tor_traefik
# ... your other networks ...
myapp:
image: myapp:latest
labels:
# Regular Traefik router (clearnet)
- "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
# Tor router (onion)
- "traefik.http.routers.myappTor.rule=Host(`abc123...xyz.onion`)"
- "traefik.http.routers.myappTor.entrypoints=webnorclear"
- "traefik.http.routers.myappTor.tls=false"
networks:
- priv_tor_traefik
volumes:
tor_keys:
networks:
priv_tor_traefik:
external: true
5. Retrieve your onion addresses
On first start, Tor generates cryptographic keys and .onion addresses. The container logs them automatically:
Onion Services (alphabetically sorted):
myapp.example.com:abc123...xyz.onion
anotherapp.example.com:def456...uvw.onion
You can also retrieve them manually:
docker exec tor cat /var/lib/tor/myapp.example.com/hostname
Use these addresses in your Traefik Host() rules.
Optional: Onion-Location Header
To tell Tor Browser users visiting your clearnet site that an onion version exists, add a middleware:
labels:
- "traefik.http.middlewares.onion-location.headers.customresponseheaders.Onion-Location=http://abc123...xyz.onion"
- "traefik.http.routers.myapp.middlewares=onion-location"
Tor Browser will display a ".onion available" badge in the address bar.
Image Details
| Component | Details |
|---|---|
| Base image | alpine:3.23 |
| Platforms | linux/amd64, linux/arm64, linux/arm/v7 |
| Packages | tor, curl, bash, jq |
| Runs as | tor user (non-root) |
| SOCKS proxy | 127.0.0.1:9050 (internal only) |
| Healthcheck | Every 5 min via curl --socks5-hostname 127.0.0.1:9050 against Tor Project's API |
| Startup time | Key wait is dynamic (instant if keys exist, up to 120s for new services); healthcheck polls every 10s for the first 120s |
Supported Platforms
Every tag is a multi-arch image. Docker pulls the variant that matches your host automatically, so the same image: line works everywhere.
| Platform | Typical hardware |
|---|---|
linux/amd64 |
x86_64 servers, desktops, VMs |
linux/arm64 |
Raspberry Pi 3, 4, 5 and Zero 2 W on a 64-bit OS; other ARMv8 boards; Apple Silicon (via Docker Desktop) |
linux/arm/v7 |
Raspberry Pi 2, 3, 4 and Zero 2 W on 32-bit Raspberry Pi OS; other ARMv7 boards |
linux/arm/v6 (original Raspberry Pi 1 and Pi Zero/Zero W) is not built.
To check which variant you got: docker image inspect --format '{{.Architecture}}{{if .Variant}}/{{.Variant}}{{end}}' thefizi/alpine-tor-for-traefik:latest.
All variants are built from the same Alpine base snapshot in the same CI run, so a given version tag carries the same Tor and Alpine versions on every platform. The ARM images are built under QEMU emulation rather than on native hardware.
Onion keys in /var/lib/tor are not architecture-specific, so a key volume can move between an x86 host and a Raspberry Pi and keep the same .onion addresses.
Image Tags
| Tag | Description |
|---|---|
latest |
Most recent build from main branch |
tor-{VER}-alpine-{VER} |
Pinned to specific Tor and Alpine versions |
Available from:
- Docker Hub:
thefizi/alpine-tor-for-traefik - Forgejo:
git.pickysysadmin.ca/eric/alpine-tor-for-traefik
Persisting Onion Keys
Mount a named volume or host path to /var/lib/tor so your .onion addresses survive container recreation. If you lose the keys, new addresses will be generated and the old ones will stop working.
Healthcheck
The built-in healthcheck verifies the container is routing traffic through Tor by querying https://check.torproject.org/api/ip through the local SOCKS port (127.0.0.1:9050). It reports:
- healthy:
IsTor: true-- traffic is routed through Tor - unhealthy:
IsTor: false-- Tor circuit is not established
Timing: 120-second startup grace period (polling every 10 seconds), then checks every 5 minutes with a 15-second timeout.
Building from Source
git clone https://git.pickysysadmin.ca/eric/alpine-tor-for-traefik.git
cd alpine-tor-for-traefik
docker build -t alpine-tor-for-traefik:local .