Configuration
Soli Proxy uses two files: proxy.conf for routing rules and config.toml for server settings. Both support hot reloading without downtime.
1 Routing Rules — proxy.conf
Each line maps a source pattern to one or more backend targets using the -> arrow syntax. Rules are evaluated top-to-bottom; the first match wins.
Domain-Only Rules
Match requests by their Host header. Any path on that domain is forwarded to the target.
# All traffic to example.com goes to backend on port 8080
example.com -> http://backend:8080
# Multiple domains, each with its own backend
api.example.com -> http://api-server:3000
admin.example.com -> http://admin-panel:4000
Domain + Path Prefix
Combine a domain with a path prefix to route specific URL paths on a domain to a different backend.
# Only /api/* paths on app.example.com go to the API service
app.example.com/api/* -> http://api:8082
# Everything else on app.example.com goes to the frontend
app.example.com -> http://frontend:3000
Path-Only Rules (Any Domain)
Match by URL path regardless of the host. Paths ending in /* match any subpath (prefix match). Paths without a wildcard require an exact match.
# Prefix match: /api/ and all subpaths
/api/* -> http://api:8090
# Exact match: only /health (not /health/check)
/health -> http://monitoring:9090
# WebSocket route
/ws -> ws://realtime:8888
Regex Rules
Prefix the source with ~ to use a regular expression. The regex is matched against the full request path.
# Match versioned API paths: /v1/..., /v2/..., etc.
~^/v[0-9]+/.*$ -> http://versioned:8091
# Match UUID-based resource paths
~^/resources/[0-9a-f]{8}-[0-9a-f]{4}-.*$ -> http://resources:8092
HTML URL Rewriting
When using path prefix rules (like /api/* -> http://api:8090), the proxy automatically rewrites URLs in HTML responses to include the prefix. This ensures assets loaded by the browser point to the correct path.
# Route /api/* to backend (prefix stripped from request)
/api/* -> http://api:8090
With the rule above, when the backend returns HTML like:
<link rel="stylesheet" href="/style.css">
<script src="/app.js"></script>
<form action="/submit">
The proxy rewrites it to:
<link rel="stylesheet" href="/api/style.css">
<script src="/api/app.js"></script>
<form action="/api/submit">
The rewriting applies to href, src, and action attributes in HTML responses. It also handles gzip/deflate compression transparently.
Multi-Backend (Load Balancing)
Specify multiple backends separated by commas. Use backslash \ at the end of a line to continue on the next line. Traffic is distributed across all targets.
# Two backends on a single line
/app/* -> http://b1:8080, http://b2:8080
# Line continuation with backslash for readability
/api/* -> http://backend1:8080, \
http://backend2:8080, \
http://backend3:8080
Per-Route Lua Scripts
Append @script:filename.lua to attach Lua scripts to specific routes. Multiple scripts are comma-separated.
# Single script on a route
/admin/* -> http://admin:4000 @script:auth.lua
# Multiple scripts on a route
/api/* -> http://api:3000 @script:auth.lua,rate_limit.lua
# Works with multi-backend and line continuation
/api/* -> http://a:8080, \
http://b:8080 @script:auth.lua
Global Scripts
The [global] directive applies Lua scripts to every request, regardless of the matched route.
# These scripts run on every request
[global] @script:cors.lua,logging.lua
# Routes follow as normal
/api/* -> http://api:3000 @script:auth.lua
default -> http://localhost:8080
Default Fallback
The default rule catches any request that did not match a previous rule. Place it at the end of your config.
# Catch-all fallback (must be last)
default -> http://localhost:8080
Complete Example
# Global middleware
[global] @script:cors.lua,logging.lua
# Domain-based routing
api.example.com -> http://api:3000 @script:auth.lua
admin.example.com -> http://admin:4000 @script:auth.lua
cdn.example.com -> http://static:8080
# Domain + path prefix
app.example.com/api/* -> http://api:3000
app.example.com -> http://frontend:8080
# Path-only with load balancing
/api/* -> http://backend1:8080, \
http://backend2:8080, \
http://backend3:8080
# Regex for versioned endpoints
~^/v[0-9]+/.*$ -> http://versioned:8091
# WebSocket
/ws -> ws://realtime:8888
# Catch-all fallback
default -> http://localhost:8080
Matching Order
Rules are evaluated top-to-bottom. The first rule that matches the incoming request wins. Place more specific rules (domain + path, regex) before broader ones (domain-only, default). The default rule should always be last.
2 Server Settings — config.toml
The config.toml file controls server behavior, TLS, admin API, scripting, and more. All sections are optional with sensible defaults.
[server]
Core server binding and threading options.
[server]
bind = "0.0.0.0:80" # HTTP listen address
https_port = 443 # HTTPS listen port
worker_threads = "auto" # "auto" = num CPUs, or a number
| Key | Type | Default | Description |
|---|---|---|---|
| bind | string | "0.0.0.0:80" | Address and port for HTTP |
| https_port | integer | 443 | Port for HTTPS/TLS listener |
| worker_threads | string | "auto" | Tokio worker thread count |
[tls]
TLS mode and certificate storage.
[tls]
mode = "auto" # "auto" = self-signed dev certs
# "letsencrypt" = ACME production certs
cache_dir = "./certs" # Directory to store certificates
| Key | Type | Default | Description |
|---|---|---|---|
| mode | string | "auto" | "auto" for self-signed, "letsencrypt" for ACME |
| cache_dir | string | "./certs" | Certificate storage directory |
[letsencrypt]
ACME / Let's Encrypt settings. Only used when tls.mode = "letsencrypt".
[letsencrypt]
staging = false # true = use staging ACME server (for testing)
email = "[email protected]" # Contact email for certificate notifications
terms_agreed = true # You must agree to the ACME ToS
| Key | Type | Default | Description |
|---|---|---|---|
| staging | bool | false | Use Let's Encrypt staging for testing |
| string | -- | Contact email for ACME notifications | |
| terms_agreed | bool | -- | Accept the ACME Terms of Service |
[admin]
REST Admin API for runtime management. See the Admin API docs for endpoints.
[admin]
enabled = true # Enable the admin API
bind = "0.0.0.0:9090" # Admin API listen address
api_key = "your-secret-key" # Optional: require X-API-Key header
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | false | Enable or disable the admin API |
| bind | string | "127.0.0.1:9090" | Address and port for admin API |
| api_key | string? | none | If set, all requests require X-API-Key header |
[circuit_breaker]
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 needed to close
failure_status_codes = [502, 503, 504] # HTTP codes that count as failures
| Key | Type | Default | Description |
|---|---|---|---|
| failure_threshold | integer | 5 | Failures before breaker opens |
| recovery_timeout_secs | integer | 30 | Seconds to wait before half-open probe |
| success_threshold | integer | 2 | Successes needed to close breaker |
| failure_status_codes | array | [502, 503, 504] | HTTP status codes treated as failures |
[scripting]
Lua 5.4 scripting engine. See the Scripting docs for hook details.
[scripting]
enabled = true # Enable Lua scripting engine
scripts_dir = "./scripts/lua" # Directory containing Lua scripts
hook_timeout_ms = 10 # Max execution time per hook (ms)
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | false | Enable or disable scripting engine |
| scripts_dir | string | "./scripts/lua" | Path to Lua script files |
| hook_timeout_ms | integer | 10 | Max execution time per hook call |
[logging]
Structured logging configuration.
[logging]
level = "info" # trace, debug, info, warn, error
format = "json" # "json" or "pretty"
output = "stdout" # "stdout" or "file:/var/log/soli-proxy.log"
| Key | Type | Default | Description |
|---|---|---|---|
| level | string | "info" | Log verbosity level |
| format | string | "json" | Output format (json or pretty) |
| output | string | "stdout" | Output target (stdout or file path) |
[limits]
Connection and request limits.
[limits]
max_connections = 10000 # Maximum concurrent connections
max_request_size = "10MB" # Maximum request body size
keep_alive_timeout = 30 # Keep-alive timeout in seconds
request_timeout = 60 # Total request timeout in seconds
| Key | Type | Default | Description |
|---|---|---|---|
| max_connections | integer | 10000 | Max concurrent connections |
| max_request_size | string | "10MB" | Max request body size |
| keep_alive_timeout | integer | 30 | Keep-alive timeout (seconds) |
| request_timeout | integer | 60 | Total request timeout (seconds) |
[rate_limiting]
Global rate limiting with token bucket strategy.
[rate_limiting]
enabled = true # Enable rate limiting
strategy = "token_bucket" # Rate limiting algorithm
requests_per_second = 1000 # Sustained request rate
burst_size = 2000 # Maximum burst above sustained rate
| Key | Type | Default | Description |
|---|---|---|---|
| enabled | bool | true | Enable or disable rate limiting |
| strategy | string | "token_bucket" | Rate limiting algorithm |
| requests_per_second | integer | 1000 | Sustained request rate per second |
| burst_size | integer | 2000 | Max burst above sustained rate |
Complete config.toml Example
[server]
bind = "0.0.0.0:80"
https_port = 443
worker_threads = "auto"
[tls]
mode = "letsencrypt"
cache_dir = "./certs"
[letsencrypt]
staging = false
email = "[email protected]"
terms_agreed = true
[admin]
enabled = true
bind = "127.0.0.1:9090"
api_key = "your-secret-key"
[circuit_breaker]
failure_threshold = 5
recovery_timeout_secs = 30
success_threshold = 2
failure_status_codes = [502, 503, 504]
[scripting]
enabled = true
scripts_dir = "./scripts/lua"
hook_timeout_ms = 10
[logging]
level = "info"
format = "json"
output = "stdout"
[limits]
max_connections = 10000
max_request_size = "10MB"
keep_alive_timeout = 30
request_timeout = 60
[rate_limiting]
enabled = true
strategy = "token_bucket"
requests_per_second = 1000
burst_size = 2000
3 Hot Reloading
Configuration changes are applied without restarting the proxy or dropping active connections. There are three ways to trigger a reload.
SIGUSR1 Signal
Send a Unix signal to the running process for immediate reload.
kill -USR1 $(pidof soli-proxy)
Admin API
HTTP endpoint for programmatic reload from scripts or CI/CD.
POST http://localhost:9090/reload
File Watcher
Automatic detection when proxy.conf is modified on disk.
Automatic — no action needed
SIGUSR1 Signal
Send the SIGUSR1 signal to the Soli Proxy process. This is the simplest reload method for manual operations.
# Find and signal the process
kill -USR1 $(pidof soli-proxy)
# Or if using systemd
systemctl kill --signal=USR1 soli-proxy
Admin API Reload
Call the Admin API reload endpoint. Requires the admin API to be enabled in config.toml.
# Without API key
curl -X POST http://localhost:9090/reload
# With API key authentication
curl -X POST http://localhost:9090/reload \
-H "X-API-Key: your-secret-key"
Automatic File Watcher
Soli Proxy watches the proxy.conf file for changes using OS-level file notifications (inotify on Linux, FSEvents on macOS). When a change is detected, configuration is reloaded automatically.
Smart Suppression
When routes are modified via the Admin API, the file watcher suppresses the next filesystem event to avoid a double reload. The Admin API writes to proxy.conf and swaps the in-memory config atomically.