- Dockerfile 54.7%
- Shell 45.3%
|
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
A prior commit introduced CRLF line endings across all files. CRLF in entrypoint-wrapper.sh made the kernel try to exec `bash\r`, breaking container startup with `env: can't execute 'bash': No such file or directory`. Renormalize the repo to LF and add .gitattributes to lock in `eol=lf` so this cannot regress. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|---|---|---|
| .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 monthly 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 |
| Packages | tor, torsocks, curl, bash, jq |
| Runs as | tor user (non-root) |
| SOCKS proxy | 127.0.0.1:9050 (internal only) |
| Healthcheck | Every 5 min via torsocks curl against Tor Project's API |
| Startup time | Key wait is dynamic (instant if keys exist, up to 120s for new services); healthcheck begins after 60s |
Image Tags
| Tag | Description |
|---|---|
latest |
Most recent build from main branch |
tor-{VER}-alpine-{VER} |
Pinned to specific Tor and Alpine versions |
dev |
Development builds (Forgejo registry only) |
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 via torsocks. It reports:
- healthy:
IsTor: true-- traffic is routed through Tor - unhealthy:
IsTor: false-- Tor circuit is not established
Timing: 60-second startup grace period, 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 .