Self-Hosting the Cycles Server
The Cycles server is a Spring Boot application that enforces budget reservations backed by Redis. This guide covers how to run it locally, with Docker, and in production.
Looking for the full deployment walkthrough? See Deploying the Full Cycles Stack for an end-to-end guide that covers the admin server, tenant setup, API key creation, budget allocation, and verification — everything you need from zero to a working deployment.
Prerequisites
- Docker and Docker Compose (for the quick path — no Java needed), or
- Java 21+ and Maven 3.9+ (for building from source)
- Redis 7+ (required for Lua script compatibility)
Quick start with Docker Compose
Using pre-built GHCR images (recommended)
The fastest way to get the Cycles server running. No Java or Maven required:
cd cycles-server
docker compose -f docker-compose.prod.yml up -dThis pulls ghcr.io/runcycles/cycles-server:latest and starts it with Redis.
Pinning versions: Replace
:latestwith a specific version tag (e.g.,:0.1.23) indocker-compose.prod.ymlfor reproducible deployments.
Building from source with Docker
The repository includes a multi-stage Dockerfile that builds the JAR inside Docker — no local Java or Maven needed:
cd cycles-server
docker compose up -dThis uses docker-compose.yml which builds from source via the multi-stage Dockerfile.
Full stack (with Admin Server)
To run both the Cycles Server and Admin Server together:
cd cycles-server
docker compose -f docker-compose.full-stack.yml up -d # build from source
docker compose -f docker-compose.full-stack.prod.yml up -d # use GHCR imagesThe full-stack compose files expect cycles-server-admin to be cloned alongside as a sibling directory.
The server is available at http://localhost:7878.
Verify it is running:
curl http://localhost:7878/actuator/healthRunning from source
Clone the repository and build:
git clone https://github.com/runcycles/cycles-server.git
cd cycles-server/cycles-protocol-service
mvn clean package -DskipTestsStart Redis (if not already running):
redis-serverRun the server:
java -jar cycles-protocol-service-api/target/cycles-protocol-service-api-*.jarThe server starts on port 7878 by default.
Configuration
The server is configured via environment variables or application.properties.
Environment variables
| Variable | Default | Description |
|---|---|---|
REDIS_HOST | localhost | Redis server hostname |
REDIS_PORT | 6379 | Redis server port |
REDIS_PASSWORD | (empty) | Redis password (optional) |
server.port | 7878 | HTTP server port |
cycles.expiry.interval-ms | 5000 | Interval for the background reservation expiry sweep (ms) |
Full application properties
# Server
server.port=7878
# Redis
redis.host=${REDIS_HOST:localhost}
redis.port=${REDIS_PORT:6379}
redis.password=${REDIS_PASSWORD:}
# JSON serialization
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.deserialization.fail-on-unknown-properties=true
spring.jackson.default-property-inclusion=non_null
# Reservation expiry sweep interval
cycles.expiry.interval-ms=5000
# Logging
logging.level.root=INFO
logging.level.io.runcycles.protocol=DEBUG
# OpenAPI / Swagger UI
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.enabled=true
# Actuator
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=when-authorizedRedis connection
The server uses a JedisPool with a default maximum of 50 connections. Redis 7+ is required because the Lua scripts use features not available in earlier versions.
Redis with authentication
Set the REDIS_PASSWORD environment variable:
REDIS_PASSWORD=your-redis-password java -jar cycles-protocol-service-api-*.jarRedis connection pool
The default pool configuration uses 50 max connections, which is sufficient for most workloads. For high-throughput deployments, tune the pool size by modifying the RedisConfig class or providing a custom JedisPool bean.
Background expiry sweep
The server runs a background task every 5 seconds (configurable via cycles.expiry.interval-ms) that:
- Scans the reservation TTL sorted set for expired entries
- Marks expired reservations as
EXPIRED - Releases their reserved budget back to the affected scopes
This ensures abandoned reservations (from crashed clients or network failures) do not permanently consume budget.
Health checks
The server exposes Spring Boot Actuator health endpoints:
# Basic health check
curl http://localhost:7878/actuator/health
# Detailed health (when authorized)
curl http://localhost:7878/actuator/healthSwagger UI
The server includes interactive API documentation via Swagger UI:
http://localhost:7878/swagger-ui.htmlThe raw OpenAPI spec is available at:
http://localhost:7878/api-docsProduction considerations
Stateless server
The Cycles server is stateless — all state lives in Redis. You can run multiple server instances behind a load balancer without sticky sessions.
Redis persistence
Enable Redis persistence (RDB or AOF) to survive Redis restarts without losing budget state. For production:
# redis.conf
appendonly yes
appendfsync everysecRedis memory
Budget data is compact. Each scope stores a few counters. Each active reservation stores its metadata. Typical memory usage is low unless you have millions of concurrent reservations.
Security
- Always run the server behind HTTPS in production (use a reverse proxy like nginx or a cloud load balancer)
- Use strong, unique API keys per tenant
- Set
REDIS_PASSWORDand restrict Redis network access - Consider running Redis in a private subnet not accessible from the internet
Scaling
For higher throughput:
- Add more Cycles server instances behind a load balancer
- Use Redis Cluster for horizontal scaling of budget state
- Tune the JedisPool connection count based on your concurrency needs
Verifying your deployment
After starting the server, verify the full lifecycle works. You need an API key and a budget already configured via the Cycles Admin Server:
# Create a reservation (requires a valid API key and budget for the tenant scope)
curl -s -X POST http://localhost:7878/v1/reservations \
-H "Content-Type: application/json" \
-H "X-Cycles-API-Key: $CYCLES_API_KEY" \
-d '{
"idempotency_key": "test-001",
"subject": { "tenant": "acme-corp" },
"action": { "kind": "test", "name": "verify" },
"estimate": { "amount": 100, "unit": "USD_MICROCENTS" }
}'If the server is configured correctly, you will receive a JSON response with a reservation_id and "decision": "ALLOW".
If you get BUDGET_EXCEEDED, you need to create a budget via the admin server first. If you get UNAUTHORIZED, verify your API key was created correctly. See the full stack deployment guide for the complete bootstrap sequence.
Next steps
- API Reference — interactive endpoint documentation
- Server Configuration Reference — all configuration properties
- Architecture Overview — how the components fit together
