Arcus is an open-source subdomain-as-a-service platform. It lets you sell and manage customer subdomains on your own base domain, then proxy traffic to each customer origin.
- Credit-based subdomain purchases
- Per-subdomain origin routing
- JWT auth, API tokens, admin endpoints, and webhooks
- Cloudflare DNS integration
- Traefik edge TLS with Let's Encrypt
api: FastAPI control planerouter: FastAPI reverse proxy for wildcard subdomainspostgres: data storetraefik: edge ingress and TLS termination
- Docker 24+
- Docker Compose V2
- Cloudflare zone + token (
Zone:DNS:Edit) for production edge/TLS
cp .env.example .envSet at least:
CLOUDFLARE_API_TOKENCLOUDFLARE_ZONE_IDBASE_DOMAINJWT_SECRET_KEYPOSTGRES_PASSWORD
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --buildUseful URLs:
- API docs: http://localhost:8000/docs
- API health: http://localhost:8000/health
- Router health: http://localhost:8001/health
- Local edge API: http://api.localhost/docs
- Local UI: http://api.localhost/login
Local development assumptions:
- local Traefik uses plain HTTP on
*.localhost - Cloudflare and ACME are disabled locally
- private, loopback, and LAN origin hosts are allowed locally
- use
host.docker.internalfor services running on your host machine - browser sessions are canonicalised onto
api.localhost; uselocalhost:8000for direct API access - production defaults remain strict
docker compose -f docker-compose.yml -f docker-compose.local.yml downCreate the first admin user:
curl -s -X POST http://localhost:8000/auth/setup \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"changeit123"}' | jqLog in and export the bearer token:
export ARCUS_TOKEN=$(
curl -s -X POST http://localhost:8000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"changeit123"}' | jq -r '.access_token'
)Create user:
curl -s -X POST http://localhost:8000/admin/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARCUS_TOKEN" \
-d '{"email":"alice@example.com","role":"normal"}' | jqGrant credits:
curl -s -X POST http://localhost:8000/credits/grant \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARCUS_TOKEN" \
-d '{"user_id":"<user_id>","amount":5}' | jqPurchase subdomain:
curl -s -X POST http://localhost:8000/subdomains/purchase \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARCUS_TOKEN" \
-d '{"user_id":"<user_id>","slug":"myapp"}' | jqSet origin:
curl -s -X POST http://localhost:8000/subdomains/myapp/origin \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARCUS_TOKEN" \
-d '{"origin_host":"203.0.113.10","origin_port":8080}' | jqCheck availability:
curl -s "http://localhost:8000/subdomains/check?slug=myapp" | jqRun unit/integration tests:
pytestRun Docker E2E tests:
docker compose -f docker-compose.e2e.yml up --build --abort-on-container-exit
docker compose -f docker-compose.e2e.yml down -v- Private and loopback origin IP ranges are blocked
- DNS resolution checks prevent private-network origin bypass
- Edge rate limiting is enforced by Traefik
See CONTRIBUTING.md.