A reverse proxy like nginx, built on pingora, simple and efficient.
See the codeA reverse proxy and application gateway built on Cloudflare Pingora. It is configured through TOML files or a web admin that is part of the binary, takes changes without a restart, and comes with more than thirty plugins.
中文说明 | Documentation · 中文文档 | Examples | Plugins | Crates | Upgrading
pingap -t checks it before it is stored, earlier versions can be kept and restored, and there are read-only accounts and API tokens.--autoreload). What needs new listeners is applied by a restart that hands the sockets over (--autorestart, for a daemon on Linux), and the running process keeps serving if its replacement does not come up.-t builds everything a start would, --strict refuses keys it does not know, --diff shows what a change would do. TOML, HCL or KDL, in files or in etcd, with secrets taken from the environment or from files.The rest of what a reverse proxy does is there too: HTTP/1.1, HTTP/2, WebSocket and gRPC-web; routing by host, path and regular expression; round robin, least connections, consistent hashing and sticky sessions, with health checks, retries and a circuit breaker; backends from DNS (A/AAAA, SRV) or Docker labels; the PROXY protocol on listeners and towards upstreams; OpenSSL or rustls.
Metrics, tracing and image optimisation are in the -full builds.
# docker-compose.yml
services:
pingap:
image: vicanso/pingap:latest # for production, pin a release: vicanso/pingap:<version>-full
container_name: pingap-instance
restart: always
ports:
- "80:80"
- "443:443"
volumes:
# everything Pingap keeps: configuration, certificates, cache
- ./pingap_data:/opt/pingap
environment:
- PINGAP_CONF=/opt/pingap/conf
- PINGAP_ADMIN_ADDR=0.0.0.0:80/pingap
- PINGAP_ADMIN_USER=pingap
- PINGAP_ADMIN_PASSWORD=<YourSecurePassword> # change this
command:
- pingap
- --autoreload
mkdir pingap_data
docker compose up -d
The admin is at http://localhost/pingap. Images are tagged latest, full and rustls-full, and <version>, <version>-full, <version>-rustls-full for a release. Each also has a -distroless twin without a shell, where command has to start with pingap as it does above.
curl -sSL https://raw.githubusercontent.com/vicanso/pingap/main/install.sh | sh
installs the latest release to /usr/local/bin/pingap on Linux and macOS (x86_64 and arm64). Variables in front of sh choose what is installed:
| Variable | Effect |
|---|---|
PINGAP_FULL=1 | The -full build, with every optional feature |
PINGAP_TLS=rustls | Linux: the -rustls-full build, without OpenSSL (see TLS backend) |
PINGAP_LIBC=gnu | Linux: the glibc build in place of the static musl one |
PINGAP_SERVICE=1 | Linux with systemd: also the pingap service and a starter configuration in /etc/pingap/conf; it is not enabled or started |
PINGAP_VERSION=0.15.0 | That release in place of the latest |
All assets are on the releases page.
# the certificate comes from Let's Encrypt
pingap --domain=pingap.io --upstream=192.168.1.1:3000
# or bring your own
pingap --domain=pingap.io --upstream=192.168.1.1:3000 --cert=/etc/ssl/pingap.io
--cert the certificate is ordered by HTTP-01: the domain has to resolve to this host, with port 80 reachable. It is kept in ~/.pingap/acme/<domains>.toml and used again at the next start. Do not delete it, issuing is rate limited.--cert is the certificate or the directory it is in (fullchain.pem / privkey.pem, cert.pem / key.pem and tls.crt / tls.key are found; --key names anything else). --upstream and --domain take lists separated by commas, --addr the address to listen on.--conf, which these flags cannot be combined with.[upstreams.api]
addrs = ["api.github.com:443"]
discovery = "dns"
sni = "api.github.com"
[plugins.staticServe]
category = "directory"
path = "~/Downloads"
[locations.github-api]
upstream = "api"
path = "/api"
proxy_set_headers = ["Host:api.github.com"]
rewrite = "^/api/(?<path>.+)$ /$1"
[locations.static]
plugins = ["staticServe"]
[servers.test]
addr = "127.0.0.1:6118"
locations = ["github-api", "static"]
The same can be written in HCL or KDL. Every key is described in the configuration reference.
--autoreload | A change is applied without a restart: within ten seconds from files, at once from etcd. What takes a restart is reported, and waits for one. The mode for containers |
-a, --autorestart | The same, and a restart that drops no request for what a reload cannot change, a listen address for one. For a daemon (-d) on Linux; anywhere else it reloads, and reports what waits |
-t | Builds every upstream, location, plugin, certificate and server as a start does, and binds nothing |
--strict | A key Pingap does not know, or one this build has no feature for, is an error, where it is otherwise a warning. For -t in CI |
--diff <other> | Prints what would change if <other> took the place of the configuration, credentials shown as checksums |
$ENV:NAME, $FILE:/path | As a whole value: replaced by the variable or the file when the configuration is loaded |
404.max_retries, a connection that cannot be made is tried on another backend.About 147,000 requests a second for /ping on one thread of an M4 Pro, with a 99th percentile of 116µs (wrk, 10 connections, no access log).
wrk 'http://127.0.0.1:6118/ping' --latency
Running 10s test @ http://127.0.0.1:6118/ping
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 66.41us 23.67us 1.11ms 76.54%
Req/Sec 73.99k 2.88k 79.77k 68.81%
Latency Distribution
50% 67.00us
75% 80.00us
90% 91.00us
99% 116.00us
1487330 requests in 10.10s, 194.32MB read
Requests/sec: 147260.15
Transfer/sec: 19.24MB
make dev # needs bacon: cargo install bacon
make build-web # after a change under web/, needs Node.js
make fmt && make lint && make test
make dev builds with the full features, runs Pingap on the configuration in ~/tmp/pingap with --autoreload, serves the admin on 127.0.0.1:3018 (pingap / 123123) and rebuilds when the sources change. The admin is embedded into the binary from dist/, which make build-web fills. The minimum supported Rust version is 1.96.
The default build uses OpenSSL, compiled from source. For rustls:
cargo build --release --no-default-features --features tls-rustls,full
A rustls build always offers TLS 1.2 and 1.3 with rustls' own cipher suites, so the per-server tls_min_version, tls_max_version, tls_cipher_list and tls_ciphersuites are refused when the configuration is checked. Everything else behaves the same; the differences are listed with the certificate crate. pingap --version says which backend a binary has.
Pull requests are welcome.
make fmt, make lint and make test before pushing. CI runs the same checks on every pull request.(top 24 of 25)
9,435 followers · starred Nov 2025
321 followers · starred Jan 2026
85 followers · starred Aug 2026
471 followers · starred Feb 2026
A reverse proxy like nginx, built on pingora, simple and efficient.
See the codeA reverse proxy and application gateway built on Cloudflare Pingora. It is configured through TOML files or a web admin that is part of the binary, takes changes without a restart, and comes with more than thirty plugins.
中文说明 | Documentation · 中文文档 | Examples | Plugins | Crates | Upgrading
pingap -t checks it before it is stored, earlier versions can be kept and restored, and there are read-only accounts and API tokens.--autoreload). What needs new listeners is applied by a restart that hands the sockets over (--autorestart, for a daemon on Linux), and the running process keeps serving if its replacement does not come up.-t builds everything a start would, --strict refuses keys it does not know, --diff shows what a change would do. TOML, HCL or KDL, in files or in etcd, with secrets taken from the environment or from files.The rest of what a reverse proxy does is there too: HTTP/1.1, HTTP/2, WebSocket and gRPC-web; routing by host, path and regular expression; round robin, least connections, consistent hashing and sticky sessions, with health checks, retries and a circuit breaker; backends from DNS (A/AAAA, SRV) or Docker labels; the PROXY protocol on listeners and towards upstreams; OpenSSL or rustls.
Metrics, tracing and image optimisation are in the -full builds.
# docker-compose.yml
services:
pingap:
image: vicanso/pingap:latest # for production, pin a release: vicanso/pingap:<version>-full
container_name: pingap-instance
restart: always
ports:
- "80:80"
- "443:443"
volumes:
# everything Pingap keeps: configuration, certificates, cache
- ./pingap_data:/opt/pingap
environment:
- PINGAP_CONF=/opt/pingap/conf
- PINGAP_ADMIN_ADDR=0.0.0.0:80/pingap
- PINGAP_ADMIN_USER=pingap
- PINGAP_ADMIN_PASSWORD=<YourSecurePassword> # change this
command:
- pingap
- --autoreload
mkdir pingap_data
docker compose up -d
The admin is at http://localhost/pingap. Images are tagged latest, full and rustls-full, and <version>, <version>-full, <version>-rustls-full for a release. Each also has a -distroless twin without a shell, where command has to start with pingap as it does above.
curl -sSL https://raw.githubusercontent.com/vicanso/pingap/main/install.sh | sh
installs the latest release to /usr/local/bin/pingap on Linux and macOS (x86_64 and arm64). Variables in front of sh choose what is installed:
| Variable | Effect |
|---|---|
PINGAP_FULL=1 | The -full build, with every optional feature |
PINGAP_TLS=rustls | Linux: the -rustls-full build, without OpenSSL (see TLS backend) |
PINGAP_LIBC=gnu | Linux: the glibc build in place of the static musl one |
PINGAP_SERVICE=1 | Linux with systemd: also the pingap service and a starter configuration in /etc/pingap/conf; it is not enabled or started |
PINGAP_VERSION=0.15.0 | That release in place of the latest |
All assets are on the releases page.
# the certificate comes from Let's Encrypt
pingap --domain=pingap.io --upstream=192.168.1.1:3000
# or bring your own
pingap --domain=pingap.io --upstream=192.168.1.1:3000 --cert=/etc/ssl/pingap.io
--cert the certificate is ordered by HTTP-01: the domain has to resolve to this host, with port 80 reachable. It is kept in ~/.pingap/acme/<domains>.toml and used again at the next start. Do not delete it, issuing is rate limited.--cert is the certificate or the directory it is in (fullchain.pem / privkey.pem, cert.pem / key.pem and tls.crt / tls.key are found; --key names anything else). --upstream and --domain take lists separated by commas, --addr the address to listen on.--conf, which these flags cannot be combined with.[upstreams.api]
addrs = ["api.github.com:443"]
discovery = "dns"
sni = "api.github.com"
[plugins.staticServe]
category = "directory"
path = "~/Downloads"
[locations.github-api]
upstream = "api"
path = "/api"
proxy_set_headers = ["Host:api.github.com"]
rewrite = "^/api/(?<path>.+)$ /$1"
[locations.static]
plugins = ["staticServe"]
[servers.test]
addr = "127.0.0.1:6118"
locations = ["github-api", "static"]
The same can be written in HCL or KDL. Every key is described in the configuration reference.
--autoreload | A change is applied without a restart: within ten seconds from files, at once from etcd. What takes a restart is reported, and waits for one. The mode for containers |
-a, --autorestart | The same, and a restart that drops no request for what a reload cannot change, a listen address for one. For a daemon (-d) on Linux; anywhere else it reloads, and reports what waits |
-t | Builds every upstream, location, plugin, certificate and server as a start does, and binds nothing |
--strict | A key Pingap does not know, or one this build has no feature for, is an error, where it is otherwise a warning. For -t in CI |
--diff <other> | Prints what would change if <other> took the place of the configuration, credentials shown as checksums |
$ENV:NAME, $FILE:/path | As a whole value: replaced by the variable or the file when the configuration is loaded |
404.max_retries, a connection that cannot be made is tried on another backend.About 147,000 requests a second for /ping on one thread of an M4 Pro, with a 99th percentile of 116µs (wrk, 10 connections, no access log).
wrk 'http://127.0.0.1:6118/ping' --latency
Running 10s test @ http://127.0.0.1:6118/ping
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 66.41us 23.67us 1.11ms 76.54%
Req/Sec 73.99k 2.88k 79.77k 68.81%
Latency Distribution
50% 67.00us
75% 80.00us
90% 91.00us
99% 116.00us
1487330 requests in 10.10s, 194.32MB read
Requests/sec: 147260.15
Transfer/sec: 19.24MB
make dev # needs bacon: cargo install bacon
make build-web # after a change under web/, needs Node.js
make fmt && make lint && make test
make dev builds with the full features, runs Pingap on the configuration in ~/tmp/pingap with --autoreload, serves the admin on 127.0.0.1:3018 (pingap / 123123) and rebuilds when the sources change. The admin is embedded into the binary from dist/, which make build-web fills. The minimum supported Rust version is 1.96.
The default build uses OpenSSL, compiled from source. For rustls:
cargo build --release --no-default-features --features tls-rustls,full
A rustls build always offers TLS 1.2 and 1.3 with rustls' own cipher suites, so the per-server tls_min_version, tls_max_version, tls_cipher_list and tls_ciphersuites are refused when the configuration is checked. Everything else behaves the same; the differences are listed with the certificate crate. pingap --version says which backend a binary has.
Pull requests are welcome.
make fmt, make lint and make test before pushing. CI runs the same checks on every pull request.(top 24 of 25)
9,435 followers · starred Nov 2025
321 followers · starred Jan 2026
85 followers · starred Aug 2026
471 followers · starred Feb 2026