No description
  • Dockerfile 54.7%
  • Shell 45.3%
Find a file
Eric Schewe 8a4332289f
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
Normalize line endings to LF, add .gitattributes
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>
2026-05-18 10:27:38 -07:00
.forgejo/workflows Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
.gitattributes Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
.gitignore Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
CLAUDE.md Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
Dockerfile Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
entrypoint-wrapper.sh Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
LICENSE Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
README.md Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
SECURITY.md Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00

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)
  1. The Tor container runs a Tor client that registers onion services defined in your torrc.
  2. Each HiddenServicePort directive forwards traffic to a Traefik entrypoint that serves plain HTTP (no TLS redirect), because Tor already provides end-to-end encryption.
  3. Traefik routes the request to the correct backend using standard Host() rules matching the .onion address.

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 .

License

GNU General Public License v3.0