Configuration that helps catch typos before production does. Correlated logs and trace IDs that turn debugging into a lookup — so you know why a backend failed, not just that it did.
Quick install:
$cat /etc/ferron/ferron.conf
{
log /var/log/ferron/access.log
error_log /var/log/ferron/error.log
}
api.example.com {
proxy http://localhost:3000/
json_errors
}
$sudo systemctl reload ferron; curl https://api.example.com/v1/test
{
"status": 200,
"message": "Hello World!"
}
$cat /etc/ferron/ferron.conf
{
log /var/log/ferron/access.log
error_log /var/log/ferron/error.log
}
api.example.com {
proxy http://localhost:5000/
json_errors
}
$sudo systemctl reload ferron; curl https://api.example.com/v1/test
{"detail":"The server, acting as a gateway, received an invalid response.","status":502,"title":"Bad Gateway","trace_id":"6dea16fb9aa089a37bd163a5dc232785","type":"about:blank"}
$cat /var/log/ferron/access.log /var/log/ferron/error.log | grep 6dea16fb9aa089a37bd163a5dc232785
[2026-08-17 06:20:27.487 WARN] [trace=6dea16fb9aa089a37bd163a5dc232785] Reverse proxy: TCP connect to 127.0.0.1:5000 failed: Connection refused (os error 111)
...
[2026-08-17 06:20:27.487 WARN] [trace=6dea16fb9aa089a37bd163a5dc232785] Reverse proxy: TCP connect to [::1]:5000 failed: Connection refused (os error 111)
[2026-08-17 06:20:27.487 ERROR] [trace=6dea16fb9aa089a37bd163a5dc232785] Reverse proxy: Bad gateway — upstream: http://localhost:5000: Connect failed: TCP connect failed: Connection refused (os error 111)
See how Ferron turns debugging into a lookup with correlation.
Your web server should be clear and predictable — not something you have to wrestle with to find out what went wrong.
One minor directive change silently breaks unrelated routes. As your configuration grows over time, every deployment feels unpredictable.
Tracing a single failure across distributed services is slow, manual work. Without log correlation, you're left grepping timestamps just to guess what went wrong.
When production dips, you shouldn't have to investigate by hand. Did the config reload succeed? Is the backend healthy? You may have to SSH just to see the server status.
When the circuit breaker gets tripped, you shouldn't have to manually correlate logs to find out why, and end up guessing instead of doing a lookup.
Readable defaults and few directives, with consistent behavior even as your setup grows.
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
server_tokens off;
# Assuming you use Certbot for automatic certificate management
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/html;
gzip on;
location / {
try_files $uri $uri/ =404;
}
} # TLS certificate is obtained automatically for public hosts
example.com {
root /var/www/html
} upstream backend {
server localhost:3000;
keepalive 32;
}
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
server_tokens off;
# Assuming you use Certbot for automatic certificate management
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
} # TLS certificate is obtained automatically for public hosts
example.com {
proxy http://localhost:3000
} server {
listen 80;
listen 443 ssl http2;
server_name example.com;
server_tokens off;
# Assuming you use Certbot for automatic certificate management
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/html;
index index.php index.html index.htm;
location / {
gzip on;
try_files $uri $uri/ =404;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
fastcgi_index index.php;
# The "fastcgi.conf" is located in the NGINX configuration directory (at least on Debian-based systems)
include fastcgi.conf;
}
} # TLS certificate is obtained automatically for public hosts
example.com {
root /var/www/html
# Check if the PHP-FPM socket file is accessible by the web server user, often "ferron"
fcgi_php "unix:///var/run/php/php-fpm.sock"
} Validate configuration and inspect runtime state for daily server management.
$ferron doctor -c /etc/ferron/ferron.conf
[2026-07-11 20:26:18.807 WARN] Best practice violation (block 'http port 8443' in file '/etc/ferron/ferron.conf' at line 2, column 19): OTLP configured without an explicit `service_name`; data might be attributed incorrectly for multi-service environments
[2026-07-11 20:26:18.807 WARN] Best practice violation (block 'http port 8443' in file '/etc/ferron/ferron.conf' at line 19, column 5): `directory_listing` exposes generated indexes for directories without index files; enable it only for intentionally public file listings
Configuration best-practice warnings from ferron doctor.
When things break, you see what's happening: live runtime state with no extra setup.
A Grafana dashboard over Ferron's live metrics.
When latency rises or backends fail, follow trace IDs across logs and metrics to see what happened.
Watch latency, error rates, circuit breakers, and backend health as they change.
Monitor your running state; detect manual, out-of-band edits as they happen, before they cause a broken reload.
Send logs, metrics, and traces to Grafana, Better Stack, OpenTelemetry collectors, and other observability systems.
Access and application logs carry the same trace ID. When a request fails, grep that ID across both: see the full chain without a collector.
$cat access.log error.log | grep e9bedf4ca114e998cf8d2b578ccba761
::1 - - [16/Sep/2026:14:42:10 +0200] "GET / HTTP/2.0" 502 - "-" "curl/8.22.0" "localhost:8443" "e9bedf4ca114e998cf8d2b578ccba761"
[2026-09-16 14:42:10.725 WARN] [trace=e9bedf4ca114e998cf8d2b578ccba761] Reverse proxy: TCP connect to 127.0.0.1:80 failed: Connection refused (os error 111)
[2026-09-16 14:42:10.725 WARN] [trace=e9bedf4ca114e998cf8d2b578ccba761] Reverse proxy: backend failed, retrying same upstream (1/1) — upstream: http://demo.local:80: Connect failed: TCP connect failed: Connection refused (os error 111)
[2026-09-16 14:42:10.725 ERROR] [trace=e9bedf4ca114e998cf8d2b578ccba761] Reverse proxy: Bad gateway — upstream: http://demo.local:80: Connect failed: TCP connect failed: Connection refused (os error 111)
Search across access and application logs.
One trace ID ties multiple logs. Grep it and the full sequence appears: what it returned, where it failed.
Plain text log files, compatible with logrotate, journald, and any log shipper you already have configured.
Log files need no extra setup. No collector, dashboard, nor additional process to keep running.
Steady throughput under real-world workloads; no performance expertise required to keep it stable.
Built to hold up under load, with no artificial keep-alive limits and no pool starvation.
HTTP/2 and HTTP/3 support, keeping connections stable under heavy traffic.
Low memory usage and consistent behavior as traffic scales: no thread-per-connection overhead eating into your resources.
Sensible defaults out of the box: no worker counts, connection pools, or hidden limits to tune.
Production debugging scenarios answered with Ferron capabilities.
Circuit breaker transitions are emitted as named events and tracked as a live metric, not buried in a log line you'd only find if you knew to look.
Query the admin API's /config and /status endpoints and see the running configuration and its state directly: no restart, no diffing files by hand.
Grep the trace ID out of the access log and pull every related line from the application log in one grep. No collector, nor dashboard login, just the terminal you already have open.
Spans separate the reverse proxy hop from the origin response, so the flame graph tells you which side of the wire the time went, instead of you guessing from an aggregate number.
Choose your platform and get Ferron up and running.
Using packages (recommended):
Using installer script:
$sudo bash -c "$(curl -fsSL https://get.ferron.sh/v3)"
Using Docker CLI:
$docker pull ferronserver/ferron:3 && docker run --name myferron -d -p 80:80 --restart=always ferronserver/ferron:3
Pre-built binaries:
Build from source:
Ferron is an open-source project built by developers like you. Whether you contribute or run Ferron, you can join its community.
2K+
Stars on GitHub
20+
Contributors on GitHub
50K+
Docker Hub pulls
Improve Ferron by reporting issues, suggesting features, or submitting code.
Join our community on Matrix to chat about setups, configs, and debugging with others who run Ferron.
Hear from people running Ferron, from homelabs to production.
You may want to check out what Ferron is doing. I've been using it for a few months. Highly recommend. (...) Significantly easier to set up than nginx, and by far the most effortless auto TLS integration. (...) Highly recommend using the v2 docker images though. It now uses KDL for configuration, which is much cleaner than YAML. The syntax is versatile enough that you can create a custom DSL of sorts. Ferron uses it to replicate if statements, and uses them to filter access by IP or headers.
Michael Murphy
Engineer at System76 and Pop!_OS maintainer
I just switched to @ferronweb on my pi to serve services at home. Imho ferron is just way easier to configure than anything else.
Andreas Wachter
just tried it for serving a fastapi. It's fantastic. Instant TLS via Let's Encrypt. There may be other webservers that are equally easy, but this one is certainly easier than Apache or ngninx, which I used so far. Love it.
Thomas Walther
Founded and sold an AI startup to Spotify
Install on your platform, configure with clear files, and debug with trace IDs — from first setup to production.