Rota is a self-hosted proxy rotation platform: a Go proxy server that rotates thousands of upstream proxies with health checks, pools and per-user routing, plus a real-time dashboard on TimescaleDB. One docker compose up gives you the proxy on :8000 and everything else behind a single origin.
|
π Proxy engine
|
π₯ Sources & GeoIP
|
|
ποΈ Pools
|
π€ Users & routing
|
|
π Security
|
π Dashboard
|
Everything runs behind a single entry point, so there's just one URL to open and no API URL to configure.
# 1. Clone and start β no config file needed
git clone https://github.com/alpkeskin/rota.git
cd rota
docker compose up -d # or: make up
# 2. Grab the auto-generated admin password from the logs
make password # or: docker compose logs rota-core | grep -i passwordThen open http://localhost and log in with user admin and the password
from the logs. That's it β the dashboard, API and live logs are all served from
the same origin, so it works the same whether you're on localhost or a remote
server's IP.
What's exposed:
- π Web UI + API: http://localhost (everything β
/,/api,/docs) - π Proxy:
localhost:8000(what your clients connect through)
First-boot credentials are seeded once. Leave
ROTA_ADMIN_PASSWORDunset to get a strong random password (shown in the logs), or set it in.envto pick your own. Change it anytime via Settings β Admin account.
make help lists the shortcuts: up, build, down, restart, logs, ps,
password, dev-core, dev-dashboard.
No .env is required β defaults work out of the box. Copy cp .env.example .env
only to change something. The common knobs:
| Variable | Default | Description |
|---|---|---|
SITE_ADDRESS |
:80 |
Web entry address. Set a domain for automatic HTTPS |
ROTA_ADMIN_PASSWORD |
(random) | Initial admin password; blank β generated & logged |
ROTA_ADMIN_USER |
admin |
Initial dashboard username (seeded once) |
JWT_SECRET |
(generated, stored in DB) | Dashboard session signing key. Leave unset; set only to manage rotation yourself (changing it logs everyone out) |
PROXY_PORT |
8000 |
Host port for the proxy your clients connect to |
HTTP_PORT / HTTPS_PORT |
80 / 443 |
Web entry ports (Caddy) |
DB_PASSWORD |
rota_password |
TimescaleDB password |
CORS_ALLOWED_ORIGINS |
* |
API CORS allowlist (irrelevant behind the proxy) |
TRUST_PROXY_HEADERS |
true |
Trust X-Forwarded-For/X-Real-IP for the login rate limiter. Keep true behind the bundled Caddy; set false if the API is exposed directly |
LOG_LEVEL |
info |
Log verbosity: debug, info, warn, error |
See .env.example for the full list including auth brute-force protection.
Note:
ROTA_ADMIN_USER/ROTA_ADMIN_PASSWORDare only used when the database is empty (first start). Afterwards, use Settings β Admin account.
Point a domain at the server and set one variable β Caddy obtains and renews a TLS certificate automatically:
# .env
SITE_ADDRESS=rota.example.com
DB_PASSWORD=a-strong-random-password
ROTA_ADMIN_PASSWORD=a-strong-passworddocker compose up -d --buildEverything is then served over HTTPS at https://rota.example.com β no separate
API host, no dashboard rebuild when the domain changes.
Both services are published to GitHub Container Registry on every release:
| Image | What it is |
|---|---|
ghcr.io/alpkeskin/rota |
Core β proxy server (:8000) + REST API (:8001) |
ghcr.io/alpkeskin/rota-dashboard |
Next.js dashboard (:3000), meant to sit behind a reverse proxy that forwards /api, /ws and /docs to the core (see Caddyfile) |
Run the core on its own:
# Pull from GitHub Container Registry
docker pull ghcr.io/alpkeskin/rota:latest
# Run with basic configuration
docker run -d \
--name rota-core \
-p 8000:8000 \
-p 8001:8001 \
-e DB_HOST=your-db-host \
-e DB_USER=rota \
-e DB_PASSWORD=your-password \
ghcr.io/alpkeskin/rota:latestEvery release ships the core as a single static binary for Linux (amd64, arm64),
macOS (arm64, amd64) and Windows (amd64) β see the
releases page. It runs the proxy
(:8000) and the REST API (:8001, Swagger UI at /docs); the web dashboard
is not included, so you manage proxies, pools and users through the API.
It still needs a TimescaleDB to connect to β install it natively (Timescale ships packages and a Windows installer) or point it at any existing instance.
# 1. Unpack the archive for your platform, then put the DB connection next to it
cp .env.example .env # set DB_HOST / DB_USER / DB_PASSWORD / DB_NAME (+ ROTA_ADMIN_PASSWORD)
# 2. Run β .env is read from the working directory; real env vars take precedence
./rota # Windows: rota.exe
# 3. Use it
curl -x http://localhost:8000 https://api.ipify.org
open http://localhost:8001/docschecksums.txt on the release lists the SHA-256 of every archive.
# Prerequisites: Go 1.25.3+, Node.js 20+, pnpm, and TimescaleDB reachable
# Clone the repository
git clone https://github.com/alpkeskin/rota.git
cd rota
# Start the Go core (serves API on :8001, proxy on :8000)
make dev-core # or: cd core && go run ./cmd/server
# In a second terminal, start the dashboard.
# Dev runs on separate ports, so point the browser at the core directly:
cd dashboard
cp .env.local.example .env.local # sets NEXT_PUBLIC_API_URL=http://localhost:8001
make -C .. dev-dashboard # or: pnpm install && pnpm devThe DB connection and admin credentials come from the same environment variables as the Docker setup (see
.env.example).
# Route traffic through Rota proxy
curl -x http://localhost:8000 https://api.ipify.org?format=json
# Per-user pool routing (after creating a Proxy User in the dashboard)
curl -x http://myuser:mypassword@localhost:8000 https://api.ipify.org?format=json
# Using environment variables
export HTTP_PROXY=http://localhost:8000
export HTTPS_PROXY=http://localhost:8000
curl https://api.ipify.org?format=jsonRota provides interactive API documentation. Once the stack is running, you can access it at:
http://localhost/docs
The docs interface allows you to:
- π Browse all available API endpoints
- π§ͺ Test API requests directly from your browser
- π View request/response schemas
- π Explore authentication requirements
Quick Access:
- API docs: http://localhost/docs
- OpenAPI Spec: http://localhost/api/v1/swagger.json
Rota is a monorepo. A single reverse proxy (Caddy) is the only web entry point, so the browser talks to one origin; the dashboard, API and WebSockets are all same-origin behind it. Only the proxy port is exposed separately.
Diagram sources live in docs/diagrams/ (self-contained HTML/SVG, generated with diagram-design).
- Random: Select a random proxy for each request
- Round Robin: Distribute requests evenly across all proxies
- Least Connections: Route to the proxy with fewest active connections
- Time-Based: Rotate proxies at fixed intervals
# Production configuration
docker compose -f docker-compose.yml up -d
# Enable auto-restart
docker compose up -d --restart=unless-stopped- Go to Sources in the dashboard
- Add a URL pointing to a plain-text proxy list (one
ip:portper line) - Choose the protocol and refresh interval
- Pick Fetch now from the row menu or wait for the scheduler
The system will:
- Download and parse the list
- Upsert proxies into the database (duplicates ignored)
- Automatically look up GeoIP data for every new proxy
- Re-sync all pools that have
Auto-syncenabled
After proxies are geolocated, open the Pools β Geo distribution tab:
- Browse all proxy-holding countries; click a country to expand cities
- Check individual countries or cities; mix them freely
- Click Create Pool from selection β the pool is created and filled instantly
Pools also support ISP filters (substring match, OR logic) and tag filters (AND logic β proxy must carry all specified tags). Combine geo + ISP + tags in any combination.
| Mode | Behaviour |
|---|---|
auto |
Pool membership is rebuilt automatically after every proxy import or geo-enrichment |
manual |
Membership only changes when you press Sync β useful for curated pools |
# Plain text β one protocol://ip:port per line
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost/api/v1/pools/{id}/export?format=txt" -o pool.txt
# CSV β with status, geo, ISP, success rate
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost/api/v1/pools/{id}/export?format=csv" -o pool.csvAdd an alert rule to a pool to be notified when the active proxy count drops below a threshold:
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"http://localhost/api/v1/pools/{id}/alert-rules" \
-d '{
"enabled": true,
"min_active_proxies": 10,
"webhook_url": "https://hooks.slack.com/...",
"cooldown_minutes": 30
}'Payload sent to the webhook:
{
"event": "pool.degraded",
"pool_id": 1,
"pool_name": "US Residential",
"active_proxies": 3,
"total_proxies": 50,
"threshold": 10,
"fired_at": "2026-04-02T04:30:00Z"
}Any other URL receives the generic JSON payload above. When the webhook host is
api.telegram.org, Rota instead calls the Telegram Bot API sendMessage method
and generates the message text for you β so you don't need a text=
parameter. To alert a topic inside a group:
- Create a bot with @BotFather and copy its token.
- Add the bot to your group and give it permission to post in the topic.
- Find the group's numeric
chat_id(a negative number, e.g.-1001234567890) and the topic'smessage_thread_id. - Set the alert rule's Webhook URL to:
https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<-100...>&message_thread_id=<topic_id>
message_thread_idis optional β omit it to post to the group's main channel.- The
botprefix on the token is required by Telegram; Rota adds it automatically if you leave it out. webhook_methodis ignored for Telegram (alwaysPOST).
The delivered message looks like:
π΄ Rota pool alert
Pool: US Residential (#1)
Active proxies: 8 / 9
Threshold: 9
- Create pools for each location/use-case
- Go to Users, click Add user
- Set a main pool and optional fallback pools (in priority order)
- Configure max retries across the chain and an optional
requests_per_minutecap
Users connect as:
http://username:password@your-proxy-host:8000
If the main pool has no live IPs the request automatically cascades to the next fallback pool; each retry picks a fresh proxy and skips the ones that already failed.
Turn on Export API for the user, then fetch the alive proxies of their main pool (or any pool) with the user's own credentials β handy for tools that want a raw list instead of routing through Rota:
# One proxy per line; default = the user's main pool, raw address[:user:pass]
curl "http://localhost/api/v1/proxy-users/export-working-proxies?username=myuser&password=mypassword"
# A specific pool, capped, as protocol://[user:pass@]address
curl "http://localhost/api/v1/proxy-users/export-working-proxies?username=myuser&password=mypassword&pool=US%20Residential&count=50&format=url"The dashboard builds this link for you from the row menu (Users β β β Export link). The password travels in the query string, so keep such links private.
All API endpoints require a JWT bearer token obtained from POST /api/v1/auth/login.
# Login
TOKEN=$(curl -s -X POST http://localhost/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"yourpassword"}' | jq -r '.token')
# Use token
curl -H "Authorization: Bearer $TOKEN" http://localhost/api/v1/proxiesPublic endpoints (no token required):
GET /healthPOST /api/v1/auth/login
The login endpoint has two independent rate-limit mechanisms:
| Mechanism | Trigger | Response |
|---|---|---|
| Per-IP block | β₯ AUTH_IP_MAX_ATTEMPTS failed attempts from one IP within AUTH_IP_WINDOW_MINUTES minutes |
429 β IP blocked for AUTH_IP_BLOCK_MINUTES minutes |
| Global lockout | β₯ AUTH_GLOBAL_MAX_PER_MINUTE total attempts per minute across all IPs |
429 β login disabled for everyone for AUTH_GLOBAL_LOCKOUT_MINUTES minute(s) |
Both responses include a Retry-After header. All thresholds are configurable via .env.
Behind a reverse proxy? Per-IP tracking uses the socket peer address by default. Set
TRUST_PROXY_HEADERS=true(the default in the bundled Caddy setup) so the real client IP is read fromX-Forwarded-Forinstead of the proxy's address. Leave itfalsewhen the API is exposed directly, otherwise a client could forge the header to dodge the per-IP block.
The dashboard automatically redirects to the login page with a "Session expired" message when a 401 response is received.
Contributions are welcome! We appreciate meaningful contributions that add value to the project.
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes
- Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
- Write clear, descriptive commit messages
- Add tests for new features
- Update documentation as needed
- Follow existing code style and conventions
- Ensure all tests pass before submitting PR
- One feature/fix per pull request
Note: Pull requests that do not contribute significant improvements or fixes will not be accepted.
# 1. Create feature branch
git checkout -b feature/my-feature
# 2. Make changes and test
make test
# 3. Commit changes
git add .
git commit -m "feat: add my feature"
# 4. Push and create PR
git push origin feature/my-featureThis project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Built with β€οΈ by Alp Keskin
β Star this repository if you find it useful!
