Proksi is a simple, lightweight, and easy-to-use proxy server that automatically handles SSL, HTTP, TCP and DNS traffic. It is designed to be used as a standalone proxy server or as a component in a larger system. Proksi is written in Rust and uses Pingora as its core networking library.
Of the many features Proksi offers is the ability to load balance to your infrastructure or any IP that supports your host configurations. Other features of Proksi also include:
- Automatic Docker and Docker Swarm service discovery through labels
- Kubernetes Ingress Controller, drop-in replacement to Traefik.
- Built-in most common middlewares such as OAuth, JWT, Basic Auth, Rate Limiting, CDN Caching, static files and others
- Traffic splits (canaries, A/B tests), request mirroring, retries and least-connections load balancing
- Automatic SSL through Let's Encrypt (wildcard certificates with DNS challenges) and redirection from HTTP to HTTPS
- Hosts with their own ports, plain HTTP hosts (e.g. behind another load balancer) and a catch-all host
- Distributed configuration: running many servers with the same routes and certificates (Redis, SQLite, Postgres)
- TCP proxying for anything that isn't HTTP, with or without TLS
- An admin UI: live traffic from clients to upstreams, the latest requests, and hosts and their rules to add and change
- Configuration through HCL with support for functions (get environment variables, etc)
- Powerful plugin system for adding new middlewares and other features using WebAssembly (WASM)
- Many others.
See the admin UI documentation for a tour of its pages.
- Install Proksi:
# Option 1: Download the latest release for your system
# https://github.com/luizfonseca/proksi/releases
# Option 2: Install it with cargo
cargo install proksi
# Option 3: Run it with Docker
docker run -d -p 80:80 -p 443:443 -v /path/to/config:/etc/proksi/ luizfonseca/proksi- Create a configuration file named
proksi.hcl - Add the following content to the file:
lets_encrypt {
enabled = true
email = "my@email.com"
}
paths {
# Where to save certificates?
lets_encrypt = "./"
}
# The hosts Proksi should handle
# You might need to edit your /etc/hosts file here.
host "mysite.localhost" {
# Will create a self-signed certificate for mysite.localhost
tls {
self_signed_on_failure = true
}
# Where to point mysite.localhost to
location "/" {
upstream "example.com:443" {
sni = "example.com"
headers {
add = { "Host" = "example.com" }
}
}
}
}- Run Proksi with your configuration:
# Option 1: Specify exact config file path
proksi -c /path/to/your/proksi.hcl
proksi -c /path/to/your/config.yml
# Option 2: Specify directory (looks for proksi.hcl, proksi.yml, proksi.yaml)
proksi -c /path/to/config-directory/
# Option 3: No config (uses minimal defaults with Let's Encrypt disabled)
proksiProksi supports flexible configuration file loading:
- Direct file paths: Specify any filename with a supported extension (
.hcl,.yml,.yaml) - Directory paths: Proksi will look for
proksi.hcl,proksi.yml, orproksi.yamlin the specified directory - No configuration: If no
-cflag is provided, Proksi uses minimal default settings with Let's Encrypt disabled and emits a warning
Proksi is configured through HCL (recommended), YAML or environment variables. Below is everything you need to get going; the documentation goes into more detail.
All of these are optional, the values below are the defaults:
service_name = "proksi"
# Defaults to 80% of the available CPUs (at least 2)
worker_threads = 4
# Where hosts are served when they don't set `listen` addresses
server {
https_address = "0.0.0.0:443"
# HTTP requests are redirected to HTTPS (and used for Let's Encrypt challenges),
# except for hosts with `tls = false`
http_address = "0.0.0.0:80"
# Behind a load balancer: the client of a request is the address it forwards in
# X-Forwarded-For (for rate limits and access logs)
# trusted_proxies = ["10.0.0.0/8"]
# When stopping, open connections get this long to finish (proksi stops as soon as they
# have). Give docker or kubernetes a little longer before they kill it, e.g.
# `stop_grace_period: 35s` in compose, or `terminationGracePeriodSeconds: 35`.
# shutdown_timeout_secs = 30
# Timeouts toward clients, in seconds (these are the defaults)
# timeouts {
# idle_secs = 60 # waiting for a request's headers, and between requests of a connection
# read_secs = 60 # waiting for each read of a request body, and for websocket messages
# write_secs = 60 # waiting for a client to read each write of a response
# }
}
logging {
enabled = true
level = "info" # debug, info, warn, error
format = "json" # json or pretty
access_logs_enabled = true
error_logs_enabled = true
# path = "/var/log/proksi" # write logs to files instead of stdout
# rotation = "daily" # never, hourly, daily, minutely
}Every host Proksi accepts requests for is a host block. The label is matched against the Host header (case-insensitive), and it's also the name certificates are issued for.
Inside a host, location blocks decide where requests go based on their path:
host "api.example.com" {
location "/login" {
upstream "auth:8080" {}
}
location "/" {
upstream "app:8080" {}
}
}- The most specific location wins:
/loginbeats/, a static path beats a parameter (/users/:id), which beats a wildcard (/files/*). - A location matches its path and everything below it:
/loginserves/login,/login/and/login/callback, but not/logins. - A request matching no location gets a
404.
Each host decides where it's served with listen. Without it, a host is served on server.https_address (443), and HTTP requests are redirected to it. Proksi only opens the ports its hosts need.
A host with tls = false is served over plain HTTP (on server.http_address without listen), and no certificate is issued for it. This is useful behind another proxy or load balancer that already handles TLS, or when running Proksi on an IP address.
host "*" serves requests for any name that doesn't have a host of its own (it needs tls = false, as certificates are issued per name):
# Plain HTTP on port 8080 for any name: Proksi doesn't open 443 at all
host "*" {
tls = false
listen = ["0.0.0.0:8080"]
location "/" {
upstream "app:8080" {}
}
}
# HTTPS on 8443, only for this host
host "internal.example.com" {
listen = ["10.0.0.1:8443"]
location "/" {
upstream "internal:3000" {}
}
}A port is either HTTP or HTTPS: a host with TLS and a host without it can't share an address. Hosts with TLS also need port 80 (server.http_address), for redirects and Let's Encrypt challenges.
tcp blocks forward TCP connections, for services that don't speak HTTP. At the top level they are plain TCP; inside a host, TLS is terminated with the host's certificate (or forwarded untouched with tls = "passthrough"):
# Plain TCP
tcp "game" {
listen = "0.0.0.0:7777"
upstream "10.0.0.1:7777" {}
}
# TLS with the certificate of db.example.com, plain TCP to the upstream
host "db.example.com" {
tcp "postgres" {
listen = "0.0.0.0:5433"
upstream "10.0.0.5:5432" {}
}
}tcp blocks can also share the HTTPS port: with listen = "0.0.0.0:443", connections asking for the host's name are forwarded while every other name is still HTTPS. If the host also has locations, alpn = ["..."] tells them apart by the protocol the client asks for. See the TCP documentation for more.
Upstreams are where requests are sent. They are labelled by their address (host:port, or [ipv6]:port) and requests are load balanced between them (round-robin, in proportion to their weight), with health checks running in the background:
host "app.example.com" {
location "/" {
upstream "10.0.0.1:3000" {
weight = 2 # twice the requests of the others
}
upstream "10.0.0.2:3000" {}
}
# An external service using TLS
location "/docs" {
upstream "example.com:443" {
sni = "example.com"
# Headers added to the request sent to the upstream
headers {
add = { "Host" = "example.com" }
}
}
}
}With Let's Encrypt enabled, Proksi issues and renews a certificate for every host automatically:
lets_encrypt {
enabled = true
email = "my@email.com"
# Use the staging environment while testing, then switch to `false`
staging = true
}
paths {
lets_encrypt = "/etc/proksi/letsencrypt"
}
host "example.com" {
location "/" {
upstream "app:8080" {}
}
}You can also bring your own certificate, or fall back to a self-signed one (useful for development):
host "example.com" {
tls {
certificate {
pem = "/etc/proksi/certs/example.com.pem"
key = "/etc/proksi/certs/example.com.key"
}
self_signed_on_failure = true
}
location "/" {
upstream "app:8080" {}
}
}The pem file holds the certificate followed by its intermediates (a "fullchain"), which proksi sends to clients along with it. Both files are checked every 10 seconds: a renewed certificate is served without restarting proksi (a certificate and a key that don't match yet, while they're being replaced, are skipped until they do).
Headers can be added or removed from responses, for a whole host or a single location (a location's headers come after the host's):
host "example.com" {
headers {
response {
add = { "X-Api-Version" = "2" }
remove = ["Server"]
}
}
location "/" {
upstream "app:8080" {}
}
}Built-in plugins are enabled per location, and run in order of their priority (see the HCL documentation):
host "example.com" {
location "/" {
upstream "app:8080" {}
# Adds an X-Request-Id header to requests and responses
request_id {}
# Only these clients (addresses or networks), the others get a 403
ip_filter {
allow = ["10.0.0.0/8"]
}
# Pages of these origins can call the location from the browser
cors {
origins = ["https://app.example.com"]
}
# 120 requests per minute for each client (IP address), then a 429
rate_limit {
requests = 120
per = "1m"
}
# Request bodies over 1MiB get a 413
body_limit {
max = "1MiB"
}
# Responses compressed with zstd, brotli or gzip, as the client accepts
compress {}
}
}Locations can also require their clients to sign in:
host "example.com" {
# A user and password, from environment variables
location "/admin" {
upstream "app:8080" {}
basic_auth {
user = env("BASIC_AUTH_USER")
pass = env("BASIC_AUTH_PASS")
}
}
# Or ask an auth service (Authelia, Authentik, oauth2-proxy) about each request
location "/internal" {
upstream "app:8080" {}
forward_auth {
url = "http://authelia:9091/api/authz/forward-auth"
response_headers = ["Remote-User"]
}
}
# Or sign in with GitHub
location "/" {
upstream "app:8080" {}
oauth2 {
provider = "github"
client_id = env("OAUTH_CLIENT_ID")
client_secret = env("OAUTH_CLIENT_SECRET")
jwt_secret = env("OAUTH_JWT_SECRET")
validations = [{ key = "email", value = ["me@example.com"] }]
}
}
}Locations can also answer without an upstream, with a redirect (to = "https://example.com{uri}") or a fixed response (respond, such as status = 503 and body = "Back soon").
A rule is a condition on a request that can evaluate to a desired effect. Rules go in the server, a host or a location, and run in order:
host "shop.example.com" {
location "/api" {
upstream "api:4000" {}
# Reads stay unlimited; writes are limited per client
rule "writes" {
and {
method {
not_in = ["GET", "HEAD", "OPTIONS"]
}
}
then {
rate_limit {
requests = 60
per = "1m"
}
}
}
}
}Conditions combine with and, or and not, on the method, host, path, query, headers, cookies or client; then takes the blocks a location takes, and can change the request's method and headers. See the rules documentation for more examples on how powerful the rule system can be and its performance.
Responses can be cached in memory or on disk:
host "example.com" {
location "/" {
upstream "app:8080" {}
cache {
enabled = true
cache_type = "disk" # or "memcache"
path = "/tmp/proksi-cache"
expires_in_secs = 3600
}
}
}
# Memory used by the disk cache to keep popular responses (default: 256MB)
cache {
memory_limit_mb = 256
}Proksi can find your containers (or Docker Swarm services) through labels, without touching the configuration file:
docker {
enabled = true
mode = "container" # or "swarm"
endpoint = "unix:///var/run/docker.sock"
interval_secs = 15
}services:
web:
image: nginxdemos/hello
labels:
proksi.enabled: "true"
proksi.host: "mysite.localhost"
proksi.port: "80"
proksi.ssl_certificate.self_signed_on_failure: "true"Routes from labels are kept when a container stops (requests get a 503) and are removed when Proksi restarts.
Proksi can run as a Kubernetes ingress controller, serving the Ingress resources of class proksi and sending requests straight to the ready pods of each service:
helm install proksi deploy/helm/proksi --namespace proksi --create-namespace
# or: kubectl apply -f https://raw.githubusercontent.com/luizfonseca/proksi/main/deploy/kubernetes/proksi.yamlSee the Kubernetes documentation for paths, TLS and running more than one replica.
To run more than one Proksi server with the same routes and certificates, use a shared store. Routes in the store take priority over the configuration file and docker labels, and every server syncs them every sync_interval_secs:
store {
store_type = "redis" # memory (default), redis, sqlite, postgres
redis_url = "redis://localhost:6379"
# database_url = "postgres://user:pass@localhost/proksi" # for sqlite and postgres
sync_interval_secs = 10
}SQLite and Postgres need Proksi to be built with the store-sqlite / store-postgres features.
With an admin block, Proksi serves Prometheus metrics (requests, latency and upstream errors per host, location and upstream) on an address of its own:
admin {
listen = "127.0.0.1:7555"
# sent as `Authorization: Bearer <secret>`; read-only unless `access = "write"`
token "grafana" {
secret = env("PROKSI_ADMIN_TOKEN")
}
}Without a token block, listen must be a loopback address. The same address serves a UI, which signs in with a token. Live numbers (requests per second, p50/p90/p99 latency, errors and upstream health over the last minutes) are at /api/v1/stats, or streamed every second from /api/v1/stats/stream. See the admin documentation for the metrics and a Prometheus configuration.
Proksi can watch the configuration file and apply changes on the fly. Changes to hosts are applied without restarting; an invalid file is ignored (and logged), so Proksi keeps running:
auto_reload {
enabled = true
interval_secs = 30
# Extra paths to watch (e.g. files used with import())
paths = ["/etc/proksi/sites"]
}HCL configurations can use a few functions:
env("NAME"): reads an environment variable (fails if it's not set)import("./sites/example.com.hcl"): includes another HCL filenum_cpus(): the number of CPUs available
# file: sites/example.com.hcl
location "/" {
upstream "app:8080" {}
}worker_threads = num_cpus()
# One file per host
host = {
"example.com" = import("./sites/example.com.hcl")
}Every setting can also be set (and overridden) through environment variables starting with PROKSI_, using __ for nested keys, e.g. PROKSI_LOGGING__LEVEL=debug or PROKSI_LETS_ENCRYPT__EMAIL=my@email.com.
The same configuration in YAML:
lets_encrypt:
enabled: true
email: "my@email.com"
host:
api.example.com:
location:
/login:
upstream:
"auth:8080": {}
/:
upstream:
"app:8080": {}See the features page for what's available today.
Documentation for Proksi can be found at https://docs.proksi.info which is also available in the docs folder of this repository (served by docs.page).
We welcome contributions to Proksi. If you have any suggestions or ideas, please feel free to open an issue or a pull request on the GitHub repository and follow the guidelines in the Contributing guide.
Proksi is licensed under the MIT License, the Apache License 2.0 and is free to use and modify.