1 TLS Modes
Soli Proxy supports two TLS modes. Switch between them with a single configuration line.
Auto Mode
DefaultGenerates a self-signed certificate for localhost on startup. Perfect for local development -- HTTPS works immediately with no external dependencies.
# Self-signed cert for localhost
[tls]
mode = "auto"
Let's Encrypt Mode
ProductionAutomatic ACME HTTP-01 challenge validation for all configured domains. Certificates are issued, stored, and renewed without any manual intervention.
[tls]
mode = "letsencrypt"
cache_dir = "./certs"
[letsencrypt]
staging = false
email = "[email protected]"
terms_agreed = true
2 Certificate Auto-Provisioning
The entire certificate lifecycle is fully automated -- from domain discovery to renewal.
Domain Discovery
Domains are collected from two sources: hostnames in proxy.conf routing rules and domain fields in sites/*/app.infos files. All unique domains are queued for certificate provisioning.
ACME Account
An ACME account is created on first run and persisted to certs/account_credentials.json. On subsequent starts, the existing account is restored automatically.
HTTP-01 Challenge Validation
The proxy intercepts requests to /.well-known/acme-challenge/ and serves the ACME token response. This happens transparently before any backend routing.
Certificate Issuance & SNI Routing
Once validated, the certificate and private key are stored per-domain. The TLS acceptor uses SNI (Server Name Indication) to serve the correct certificate for each incoming connection.
Auto-Renewal
A background task checks every 12 hours whether any certificate is within 30 days of expiry. Certificates nearing expiry are renewed automatically with zero downtime.
certs/
account_credentials.json # ACME account keypair
example.com/
cert.pem # Full certificate chain
key.pem # Private key
api.example.com/
cert.pem
key.pem
myapp.example.com/
cert.pem
key.pem
3 Circuit Breaker
Per-backend circuit breakers track health and automatically stop routing traffic to failing backends.
Closed
Healthy
Traffic flows normally. Failures are counted. When consecutive failures reach failure_threshold, the breaker opens.
Open
Unhealthy
All requests to this backend are rejected immediately (503). After recovery_timeout_secs, the breaker moves to half-open.
Half-Open
Probing
A single probe request is allowed through. If success_threshold consecutive successes occur, the breaker closes. Any failure re-opens it.
State Transitions
# Circuit Breaker Configuration
# Tracks backend health and stops sending traffic to failing backends.
# When all targets for a route are open, returns 503 Service Unavailable.
[circuit_breaker]
failure_threshold = 5 # consecutive failures before opening
recovery_timeout_secs = 30 # seconds before probing an open backend
success_threshold = 2 # successful probes to close breaker
failure_status_codes = [502, 503, 504] # HTTP codes that count as failures
Multi-Target Load Balancing
When a route has multiple targets, Soli Proxy supports three load balancing strategies. Use @lb:round-robin, @lb:weighted, or @lb:failover to specify the strategy. Default is round-robin.
# Round-robin load balancing (default)
api.example.com -> http://backend-1:3000, http://backend-2:3000
# Weighted load balancing
example.com/api/* -> http://api-primary:8080, http://api-secondary:8080 # @lb:weighted
# Failover: use second target only if first fails
example.com/backup/* -> http://primary:8080, http://backup:8080 # @lb:failover
Admin API Endpoints
/api/v1/circuit-breaker
Returns the current state of every tracked backend: state (closed/open/half_open), consecutive failures, and consecutive successes.
/api/v1/circuit-breaker/reset
Resets all circuit breakers to the closed (healthy) state. Useful after deploying a fix to a failing backend.
{
"http://backend-1:3000": {
"state": "closed",
"consecutive_failures": 0,
"consecutive_successes": 12
},
"http://backend-2:3000": {
"state": "open",
"consecutive_failures": 5,
"consecutive_successes": 0
}
}
4 Prometheus Metrics
Soli Proxy exposes a built-in /metrics endpoint in Prometheus text format, ready for scraping by Prometheus, Grafana Agent, or any compatible collector.
[metrics]
enabled = true
endpoint = "/metrics"
Sample Output
# HELP proxy_requests_total Total number of HTTP requests
# TYPE proxy_requests_total counter
proxy_requests_total 48291
# HELP proxy_requests_in_flight Number of requests currently being processed
# TYPE proxy_requests_in_flight gauge
proxy_requests_in_flight 12
# HELP proxy_bytes_received Total bytes received from clients
# TYPE proxy_bytes_received counter
proxy_bytes_received 2847103
# HELP proxy_bytes_sent Total bytes sent to clients
# TYPE proxy_bytes_sent counter
proxy_bytes_sent 91740258
# HELP proxy_response_time_seconds Average response time in seconds
# TYPE proxy_response_time_seconds gauge
proxy_response_time_seconds 0.000284
# HELP proxy_tls_connections_total Total number of TLS connections
# TYPE proxy_tls_connections_total counter
proxy_tls_connections_total 15820
# HELP proxy_errors_total Total number of proxy errors
# TYPE proxy_errors_total counter
proxy_errors_total 3
# HELP proxy_response_status_codes_total HTTP response status codes
# TYPE proxy_response_status_codes_total counter
proxy_response_status_codes_total{code="200"} 45102
proxy_response_status_codes_total{code="301"} 2040
proxy_response_status_codes_total{code="404"} 1146
proxy_response_status_codes_total{code="503"} 3
Metrics Reference
| Metric | Type | Description |
|---|---|---|
proxy_requests_total |
counter | Total number of HTTP requests processed |
proxy_requests_in_flight |
gauge | Number of requests currently being processed |
proxy_bytes_received |
counter | Total bytes received from clients |
proxy_bytes_sent |
counter | Total bytes sent to clients |
proxy_response_time_seconds |
gauge | Average response time in seconds |
proxy_tls_connections_total |
counter | Total number of TLS connections established |
proxy_errors_total |
counter | Total number of proxy errors encountered |
proxy_response_status_codes_total |
counter | HTTP response status codes with code label |
# Worker Privilege Dropping
By default, app workers inherit the same privileges as the Soli Proxy process. If you run the proxy as root (to bind ports 80/443), all workers also run as root. This is a security risk — a compromised app gives attackers full system access.
Configuring Per-App Users
Use the user and group fields in app.infos to run each app as a different user:
# Run app workers as a specific user
name = "myapp"
domain = "myapp.example.com"
start_script = "npm start"
user = "nobody"
group = "nogroup"
Never run app workers as root in production
Always configure a dedicated non-root user for each app. This limits the blast radius if an app is compromised.