SOCKS5 proxy pool: HAProxy + N Tor exit nodes in Docker with dynamic service discovery.
Client (SOCKS5) -> HAProxy (TCP :1080, leastconn) -> tor (N replicas, SOCKS5 :9050)
^
Discovery (every 15s) -> HAProxy Runtime API (:5555): add/enable/del backends dynamically
Rotator (every 60s) -> DNS -> all replica IPs -> SIGNAL NEWNYM via ControlPort
Each Tor node gets a fresh circuit <= 60s via MaxCircuitDirtiness 60 + SIGNAL NEWNYM.
New connections use new circuits; in-flight connections are not interrupted.
Backends are discovered dynamically via Docker DNS and HAProxy Runtime API.
cd multi-proxy-nodes
# Generate .env
cp .env.example .env
# Edit .env - set CONTROL_PASSWORD
# openssl rand -base64 24 (use output as CONTROL_PASSWORD)
# Start with 5 Tor replicas
docker compose up -d --build --scale tor=5
# Wait for bootstrap
echo "Waiting 120s..."
sleep 120
# Test
curl -s -x socks5h://127.0.0.1:1080 https://check.torproject.org/api/ip
# {"IsTor": true, "IP": "some.tor.exit.ip"}
# Stats
curl -s -u "admin:changeme" http://127.0.0.1:8404/stats | grep "UP"Or one-shot: bash scripts/setup.sh 5
| Variable | Default | Purpose |
|---|---|---|
CONTROL_PASSWORD |
required | Tor ControlPort auth |
STATS_PASSWORD |
changeme |
HAProxy stats page auth |
ROTATE_INTERVAL |
60 |
Seconds between rotation cycles |
SOCKS_PORT |
1080 |
Host port for SOCKS5 |
STATS_PORT |
8404 |
Host port for stats page |
This project uses dynamic service discovery instead of hardcoded backend servers:
- Discovery container (
tor-discovery): resolves the Docker DNS nametorevery 15s (5 queries to beat DNS round-robin), then diffs against current HAProxy backends via the Runtime API (TCP :5555). New IPs getadd server+enable server; stale IPs getdel server. - Rotator (
tor-rotator): similarly resolvestorvia DNS each cycle to find all replica IPs, then sendsAUTHENTICATE + SIGNAL NEWNYMto each ControlPort every 60s.
This eliminates the need to regenerate config files when scaling (docker compose up -d --scale tor=N).
- ControlPort protected by HashedControlPassword (generated from CONTROL_PASSWORD at container start)
- Stats page requires HTTP Basic Auth
- SOCKS5 frontend (1080) has no auth - restrict to localhost via firewall if exposed externally
- All services on isolated
torpoolbridge; only HAProxy ports exposed to host - Tor container runs as non-root (
toruser, Alpine system user) - HAProxy Runtime API on TCP :5555 (internal torpool network only)
CONTROL_PASSWORDvisible indocker inspectand/proc/*/environ- use Docker Swarm secrets for production- Base images:
alpine:3.21@sha256:...,haproxy:3.0-alpine@sha256:...(pinned digests)
| Symptom | Fix |
|---|---|
| curl hangs | Tor bootstrapping - wait 2 min |
| curl returns 503 | Backends DOWN - docker compose logs haproxy or docker compose exec discovery bash -c 'echo "show servers state tor_pool" | socat - tcp:haproxy:5555' |
| Rotator shows FAIL | Wrong CONTROL_PASSWORD or Tor still bootstrapping (normal first 2 min) |
| Stats returns 401 | Use admin:changeme or check STATS_PASSWORD in .env |
# Scale up to 20 replicas (hot, no rebuild needed)
docker compose up -d --scale tor=20
# Discovery adds new backends within 30s
sleep 30
# Back to 3 replicas
docker compose up -d --scale tor=3
sleep 30docker compose down -v # removes containers + volumesdocker compose logs -f # all services
docker compose logs tor | grep -i boot # Tor bootstrap progress
docker compose logs discovery --tail 5 # latest discovery cycles
docker compose logs rotator --tail 5 # latest rotation cycle
docker compose exec discovery bash -c 'echo "show servers state tor_pool" | socat - tcp:haproxy:5555' | awk '!/^#/ && NF>0 {print $4, $5, "op=" $6}' # backends status