Ferron is a single binary that lets you configure proxying, TLS, and observability in one place, with readable configuration and predictable behavior.
Quick install:
Running a few services at home shouldn't require scattered configs, extra tooling, and constant debugging.
Even a simple reverse proxy can grow into dozens of directives, nested blocks, and hidden interactions. Small changes start to feel risky.
Certbot, cron jobs, manual renewals. One expired certificate and your whole stack goes dark. Again.
A service is down. Is it the proxy? The firewall? Port forwarding? DNS? The actual problem is rarely obvious.
Clear defaults and minimal directives, with predictable behavior even as your setup grows.
# Configuration example derived from https://docs.ntfy.sh/config/#__tabbed_16_2
server {
listen 80;
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 0; # Stream request body to backend
}
} # Configuration example derived from https://docs.ntfy.sh/config/#__tabbed_16_2
# and translated from Ferron 2 ferron.kdl to Ferron 3 ferron.conf
# Note: Ferron automatically handles both HTTP and WebSockets with proxy
ntfy.sh {
proxy http://127.0.0.1:2586
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix
https_redirect false
match is_get_topic {
request.method == "GET"
request.uri.path ~ "^/([-_a-z0-9]{0,64}$|docs/|static/)"
}
if is_get_topic {
https_redirect
}
} # Configuration example derived from https://jellyfin.org/docs/general/post-install/networking/reverse-proxy/nginx
server {
# Nginx versions 1.25+
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name jellyfin.example.org;
## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
client_max_body_size 20M;
# Comment next line to allow TLSv1.0 and TLSv1.1 if you have very old clients
ssl_protocols TLSv1.3 TLSv1.2;
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.org/chain.pem;
# use a variable to store the upstream proxy
set $jellyfin 127.0.0.1;
# Security / XSS Mitigation Headers
add_header X-Content-Type-Options "nosniff";
# Permissions policy. May cause issues with some clients
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always;
# Content Security Policy
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
# Enforces https content and restricts JS/CSS to origin
# External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.
add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; font-src 'self'";
location / {
# Proxy main Jellyfin traffic
proxy_pass http://$jellyfin:8096;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
proxy_buffering off;
}
location /socket {
# Proxy Jellyfin Websockets traffic
proxy_pass http://$jellyfin:8096;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
}
server {
listen 80;
listen [::]:80;
server_name jellyfin.example.org;
return 301 https://$host$request_uri;
} jellyfin.example.org {
proxy http://127.0.0.1:8096
# Security headers (translated from NGINX config example)
header X-Content-Type-Options "nosniff";
header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()"
header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; font-src 'self'"
} 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
} Ferron issues and renews TLS certificates automatically, without requiring separate renewal tooling or manual certificate management.
$cat /etc/ferron/ferron.conf
example.com {
root /var/www/example
}
$sudo systemctl reload ferron
# Ferron reloaded
$curl https://example.com
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test page</title>
</head>
<body>
<h1>Ferron automatic TLS works!</h1>
</body>
</html>
Watch Ferron quickly serve a website with automatic TLS setup.
When your self-hosted services break at 2 AM, you need more than a vague error. Ferron gives you the clues you need, with no extra setup.
[2026-05-02 13:17:57.619 INFO] HTTP server listening on [::]:80
[2026-05-02 13:17:57.619 INFO] HTTPS server listening on [::]:443
[2026-05-02 13:17:57.620 INFO] HTTP/3 server listening on [::]:443
[2026-05-02 13:17:57.620 INFO] ACME background task started with 1 configuration(s) for domains: wrong.ferron.sh
[2026-05-02 13:17:57.625 INFO] OCSP background task started
[2026-05-02 13:17:57.671 WARN] ACME account not found on server for https://127.0.0.1:14000/dir, recreating
[2026-05-02 13:17:57.714 INFO] ACME account created for directory https://127.0.0.1:14000/dir, contact: none
[2026-05-02 13:18:01.760 ERROR] Failed to finalize ACME order for wrong.ferron.sh: DNS NXDOMAIN
Full error: API error: Get "http://wrong.ferron.sh:5002/.well-known/acme-challenge/yJ8RWozJiZAnPXI0vY0ubg3uVcQXX... Real logs emitted by the Ferron web server.
No more Googling cryptic log messages. Ferron explains what went wrong with your TLS setup, proxy config, or anything else, in plain language.
A built-in Prometheus endpoint gives you request counts, latency, and system metrics. No extra plugins, no extra containers to manage.
Export logs, metrics, and traces via OpenTelemetry to Grafana, Loki, and other observability backends you already use.
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
Ferron keeps reverse proxy configuration, TLS, and observability understandable as your setup grows.