Configuration that catches typos before production does. Correlated logs and trace IDs that turn debugging into a simple 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: backend failed, retrying with another — upstream: http://localhost:5000: Connect failed: TCP connect 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)
Watch how Ferron turns debugging into a simple 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 like a roll of the dice.
Tracing a single failure across distributed services is a nightmare. Without log correlation, you're left grepping timestamps just to guess what went wrong.
When production dips, you shouldn't have to play detective. Did the config reload succeed? Is the backend healthy? You 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 just doing a simple lookup.
Clear defaults and minimal directives, with predictable 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 by Ferron
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 by Ferron
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 by Ferron
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"
} Safe deployments and runtime control made simple 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
"ferron doctor" showing configuration best-practice warnings.
When things break, you see exactly what's happening: live runtime state, right out of the box.
See how Ferron helps you, while you debug your infrastructure.
When latency rises or backends fail, bridge your signals to trace requests end-to-end and see exactly what happened.
Watch latency, error rates, circuit breakers, and backend health in real time.
Monitor your running state; detect manual, out-of-band edits immediately so you never suffer from silent drift or 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 by default. When a request fails, grep that ID across both: see the full chain without a collector.
$cat access.log error.log | grep 3d5b9bbc315384151b7b72d8270fddc5
>>> 3d5b9bbc315384151b7b72d8270fddc5 <<< ::1 - - [13/Jun/2026:05:22:52 +0200] "GET / HTTP/2.0" 502 - "-" "curl/8.20.0"
[2026-06-13 05:22:52.672 WARN] [trace=3d5b9bbc315384151b7b72d8270fddc5] Reverse proxy: TCP connect to localhost:3000 failed: Connection refused (os error 111)
[2026-06-13 05:22:52.672 WARN] [trace=3d5b9bbc315384151b7b72d8270fddc5] Upstream http://localhost:3000 circuit opened after 5 failures within 30s
[2026-06-13 05:22:52.672 WARN] [trace=3d5b9bbc315384151b7b72d8270fddc5] Reverse proxy: backend failed, retrying with another — upstream: http://localhost:3000: Connect failed: TCP connect failed: Connection refused (os error 111)
...
Real 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 are easy to set up. No collector, no dashboard, no additional process to keep running.
Predictable throughput under real-world workloads; no performance expertise required to keep it stable.
No dropped connections or silent backend failures under load. No artificial keep-alive limits, no pool starvation.
Full HTTP/2 and HTTP/3 support, keeping connections fast and efficient under heavy traffic.
Low memory usage and consistent behavior as traffic scales: no thread-per-connection overhead eating into your resources.
High performance out of the box. No tweaking worker counts, connection pools, or hidden limits.
Real scenarios answered with real 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 the same breath. No collector, no 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 running with a single command.
Using Docker CLI:
$docker pull ferronserver/ferron:3 && docker run --name myferron -d -p 80:80 --restart=always ferronserver/ferron:3
Ferron is an open-source project built by developers like you. Whether you're a contributor or just using Ferron, you're welcome to 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 with others who use Ferron.
Don't take our word for it. Hear from people who have seen Ferron and are using it every day.
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
Deploy in minutes, configure with clarity, and run with confidence — from first setup to production.