No description
  • Dockerfile 57.9%
  • Shell 42.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-27 22:15:34 -07:00
.forgejo/workflows Build multi-arch images for amd64, arm64 and arm/v7 2026-10-27 22:15:34 -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 Build multi-arch images for amd64, arm64 and arm/v7 2026-10-27 22:15:34 -07:00
Dockerfile Remove unused torsocks package 2026-10-27 18:50:42 -07:00
entrypoint-wrapper.sh Harden torrc path parsing against word-splitting and globbing 2026-07-02 10:36:51 -07:00
LICENSE Normalize line endings to LF, add .gitattributes 2026-05-18 10:27:38 -07:00
README.md Build multi-arch images for amd64, arm64 and arm/v7 2026-10-27 22:15:34 -07:00
SECURITY.md Remove unused torsocks package 2026-10-27 18:50:42 -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 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)
  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
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 .

License

GNU General Public License v3.0