Ferron — fast, memory-safe web server in Rust
Hands-on guide to Ferron, the memory-safe Rust web server with automatic TLS, HTTP/2 and KDL config — static files, reverse proxy, PHP and security.
Ferron is a fast, memory-safe web server written entirely in Rust, which rules out whole classes of memory bugs from the start. It handles static files, reverse proxying, PHP over FastCGI, automatic TLS and rate limiting out of the box, with no extra modules to load. Everything is configured through a single, readable KDL file (ferron.kdl) that stays far more compact than a classic Apache or NGINX setup. This guide takes you from installation and service management to the key KDL directives for TLS, proxying and security.
Installation (Debian/Ubuntu)
Add the official Ferron apt repository and install Ferron on Debian/Ubuntu.
sudo apt install curl gnupg2 ca-certificates lsb-release
curl https://deb.ferron.sh/signing.pgp | gpg --dearmor | sudo tee /usr/share/keyrings/ferron-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/ferron-keyring.gpg] https://deb.ferron.sh $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/ferron.list
sudo apt update && sudo apt install ferronDefault file locations after installing Ferron via apt on Debian/Ubuntu.
# Key paths after apt install:
# Binary: /usr/sbin/ferron
# Config: /etc/ferron.kdl
# Web root: /var/www/ferron
# Logs: /var/log/ferron/Pull and run Ferron via Docker. Image variants: '2' (distroless), '2-alpine', '2-debian'.
docker pull ferronserver/ferron:2
docker run --name ferron -d -p 80:80 --restart=always ferronserver/ferron:2Service Management (systemctl)
systemctl start ferron — Start the Ferron web server.
systemctl start ferronsystemctl stop ferron — Stop the Ferron web server.
systemctl stop ferronsystemctl restart ferron — Restart Ferron (briefly drops connections).
systemctl restart ferronsystemctl reload ferron — Reload the Ferron configuration with zero downtime. Re-reads /etc/ferron.kdl.
systemctl reload ferronsystemctl status ferron — Show Ferron service status, PID, and recent log output.
systemctl status ferronsystemctl enable ferron — Enable Ferron to start automatically on system boot.
systemctl enable ferronsystemctl disable ferron — Disable Ferron from starting automatically on boot.
systemctl disable ferronCLI Commands
ferron — Start Ferron using ferron.kdl in the current directory.
ferronferron -c /etc/ferron.kdl — Start Ferron with a specific configuration file. Default: ./ferron.kdl
ferron -c /etc/ferron.kdlferron --config-string 'localhost { root "/var/www" }' — Start Ferron with an inline KDL configuration string instead of a file.
ferron --config-string 'localhost { root "/var/www" }'ferron --config-adapter yaml-legacy -c ferron.yaml — Start Ferron with a legacy Ferron 1.x YAML config file. Adapters: kdl (default), yaml-legacy.
ferron --config-adapter yaml-legacy -c ferron.yamlferron --module-config — Display the compiled-in module configuration and available modules.
ferron --module-configferron -V — Show the Ferron version.
ferron -Vferron serve — Quickly serve the current directory over HTTP on port 3000 (no config file needed).
ferron serveferron serve -p 8080 -r /var/www/html — Serve a specific directory on a custom port.
ferron serve -p 8080 -r /var/www/htmlferron serve -l 0.0.0.0 -p 80 -r /var/www — Serve on all interfaces (-l). Default listen address is 127.0.0.1.
ferron serve -l 0.0.0.0 -p 80 -r /var/wwwferron serve -c "admin:$(ferron-passwd mysecret)" — Serve with HTTP Basic Auth. Use ferron-passwd to hash the password.
ferron serve -c "admin:$(ferron-passwd mysecret)"ferron-passwd <password> — Generate a hashed password for use with HTTP Basic Auth in ferron serve or ferron.kdl.
ferron-passwd mysecretpasswordferron-precompress /var/www/assets — Pre-compress static assets (gzip/brotli) using 64 threads for faster delivery.
ferron-precompress /var/www/assetsferron-precompress -t 8 /var/www/assets — Pre-compress assets with a specific number of parallel threads.
ferron-precompress -t 8 /var/www/assetsferron-yaml2kdl ferron.yaml ferron.kdl — Migrate a legacy Ferron 1.x YAML config to the modern KDL format.
ferron-yaml2kdl old-config.yaml ferron.kdlKDL Config: Structure & Syntax
KDL block targets define which hosts/ports a configuration block applies to. globals{} is evaluated first.
# Block targets (virtual hosts):
globals { } # Global settings (applied everywhere)
* { } # All hosts/ports
*:80 { } # All hosts on port 80 only
example.com { } # Specific domain
"192.168.1.1" { } # Specific IP address
example.com:8080 { } # Domain + port
example.com,example.org { } # Multiple domainsKDL directive syntax. Flags are bare keywords; booleans use #true/#false; strings use double quotes.
# Directive value types:
root "/var/www/html" # String
timeout 30000 # Integer (ms)
ocsp_stapling # Boolean flag (presence = true)
directory_listing #false # Explicit boolean
protocols "h1" "h2" # Multiple valuesLocation blocks match URL prefixes. remove_base=#true strips the prefix before forwarding to the backend.
location "/api" remove_base=#true {
proxy "http://localhost:3000"
}
location "/static" {
root "/var/www/static"
}Snippets define reusable configuration blocks. 'use' includes a snippet inside any block.
snippet "COMMON_HEADERS" {
header "X-Frame-Options" "DENY"
header "X-Content-Type-Options" "nosniff"
}
example.com {
use "COMMON_HEADERS"
root "/var/www/html"
}include "/etc/ferron.d/**/*.kdl" — Include additional KDL config files via glob pattern. Merged into the main config.
include "/etc/ferron.d/**/*.kdl"Static File Serving
Minimal static file server: serve files from the given directory for a domain.
example.com {
root "/var/www/example.com"
}Static site with ETag support, pre-compressed file serving, and aggressive browser caching.
example.com {
root "/var/www/html"
etag
compressed
directory_listing #false
file_cache_control "public, max-age=31536000"
}compressed — Serve pre-compressed .br and .gz files automatically when the client supports it (use ferron-precompress to generate them).
compresseddirectory_listing #true — Enable directory listing (disabled by default).
directory_listing #truefile_cache_control "public, max-age=3600" — Set the Cache-Control header for all static file responses.
file_cache_control "public, max-age=86400"TLS / HTTPS
HTTPS with manual certificate files: path to certificate and private key.
example.com {
tls "/etc/ssl/certs/example.com.crt" "/etc/ssl/private/example.com.key"
root "/var/www/html"
}Automatic TLS via Let's Encrypt (HTTP-01 challenge). Ferron manages certificate acquisition and renewal.
example.com {
auto_tls
auto_tls_contact "admin@example.com"
root "/var/www/html"
}On-demand TLS: acquire certificates for any domain on first request (useful for wildcard/multi-tenant setups).
globals {
auto_tls_on_demand #true
auto_tls_contact "admin@example.com"
}Global TLS hardening: minimum version, cipher suite, and OCSP stapling.
globals {
tls_min_version "TLSv1.2"
tls_cipher_suite "TLS_AES_256_GCM_SHA384"
ocsp_stapling
}Enable HTTP/1.1 and HTTP/2. Add "h3" for experimental HTTP/3 support.
globals {
protocols "h1" "h2"
}Reverse Proxy & Load Balancing
Basic reverse proxy: forward all requests to a backend. WebSocket support is included automatically.
example.com {
proxy "http://localhost:3000"
}Reverse proxy to a Unix socket backend. The URL sets the Host header; unix= sets the connection target.
example.com {
proxy "http://localhost:3000" unix="/run/app/web.sock"
}Load balance across multiple backends (round-robin by default). lb_health_check enables passive health checks.
example.com {
proxy "http://backend1:8080"
proxy "http://backend2:8080"
proxy "http://backend3:8080"
lb_health_check
}lb_algorithm "round_robin" — Set the load balancing algorithm. Options: round_robin (default), random, ip_hash.
lb_algorithm "ip_hash"proxy_keepalive — Enable keep-alive connections to the backend for improved performance.
proxy_keepaliveproxy_http2_only — Force HTTP/2-only proxying to the backend. Required for gRPC backends.
proxy_http2_onlyproxy_request_header_replace "Host" "{header:Host}" — Pass the original client Host header to the backend (Ferron replaces it by default).
proxy_request_header_replace "Host" "{header:Host}"Mixed setup: proxy /api to a backend, serve everything else as static files.
example.com {
location "/api" remove_base=#true {
proxy "http://localhost:3000"
}
location "/" {
root "/var/www/html"
}
}Enable in-memory response caching for proxied requests. cache_vary adds cache variation by header.
cache
cache_max_entries 1024
cache_vary "Accept-Encoding"PHP / FastCGI
Serve PHP via PHP-FPM using a Unix socket. Requires the fcgi module.
example.com {
root "/var/www/html"
fcgi_responder "unix:/run/php/php8.3-fpm.sock"
}Serve PHP via PHP-FPM using TCP. Use TCP when FPM runs on a different host.
example.com {
root "/var/www/html"
fcgi_responder "127.0.0.1:9000"
}Front-controller pattern for PHP frameworks (Laravel, Symfony, WordPress): route all requests through index.php.
example.com {
root "/var/www/html"
rewrite "^/index\.php/?(.*)$" "/$1" last=#true
location "/" {
rewrite "^/(.*)$" "/index.php/$1" file=#false directory=#false last=#true
}
fcgi_responder "unix:/run/php/php8.3-fpm.sock"
}Redirects & Rewrites
Redirect all HTTP traffic to HTTPS with a permanent 301 redirect.
*:80 {
return 301 "https://{header:Host}{path_and_query}"
}Redirect www to non-www (or vice versa) with a permanent redirect.
www.example.com {
return 301 "https://example.com{path_and_query}"
}return 302 "/maintenance" — Temporary redirect all requests to a maintenance page.
return 302 "/maintenance"rewrite "^/old/(.*)$" "/new/$1" last=#true — Rewrite rule with regex capture group. last=#true stops further rewrite processing.
rewrite "^/old/(.*)$" "/new/$1" last=#truerewrite "^/(.*)$" "/index.php" file=#false directory=#false last=#true — Route all requests that don't match a file or directory to index.php (front controller).
rewrite "^/(.*)$" "/index.php" file=#false directory=#false last=#trueSingle-Page Application setup: fall back to / (index.html) for all client-side routes.
# SPA: serve index.html for all non-file requests
location "/" {
root "/var/www/html"
rewrite "^/.*" "/" directory=#false file=#false last=#true
}Security & Headers
Essential security headers. Add inside a server block or snippet.
header "X-Frame-Options" "DENY"
header "X-Content-Type-Options" "nosniff"
header "Referrer-Policy" "strict-origin-when-cross-origin"
header "Strict-Transport-Security" "max-age=31536000; includeSubDomains"header "Content-Security-Policy" "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" — Content Security Policy header. Adjust sources to fit your application.
header "Content-Security-Policy" "default-src 'self'"Remove headers from responses (hide server information).
header_remove "X-Powered-By"
header_remove "Server"Block specific IP addresses or CIDR ranges. Blocked clients receive a 403 response.
block "192.168.1.100"
block "10.0.0.0/8"Allowlist IP ranges. Use in combination with block to restrict access to specific networks.
allow "10.0.0.0/8"
allow "192.168.1.0/24"trust_x_forwarded_for — Trust the X-Forwarded-For header from upstream proxies/load balancers for accurate client IP logging.
trust_x_forwarded_forRate Limiting
Limit to 100 requests/second per client IP, allowing bursts up to 200. Excess requests receive 429.
example.com {
limit rate=100 burst=200
}Apply tighter rate limiting to a specific path (e.g. API endpoints).
location "/api" {
limit rate=10 burst=20
proxy "http://localhost:3000"
}Conditions & Conditionals
Named condition block with is_regex. Use 'if' or 'if_not' to apply directives conditionally.
condition "IS_API" {
is_regex "{path}" "^/api/"
}
example.com {
if "IS_API" {
proxy "http://localhost:3000"
}
}is_equal checks an exact match on a placeholder value. Negate with if_not.
condition "NO_BOT" {
is_equal "{header:User-Agent}" "curl"
}
example.com {
if_not "NO_BOT" {
root "/var/www/html"
}
}is_remote_ip matches the client IP address.
condition "LOCAL" {
is_remote_ip "127.0.0.1"
}Placeholders usable in conditions, header directives, return, and rewrite targets.
# Available placeholders for conditions and headers:
# {path} - Request URI path
# {path_and_query} - Path including query string
# {method} - HTTP method (GET, POST, ...)
# {header:<name>} - Request header value
# {client_ip} - Client IP address
# {scheme} - http or httpsLogging & Observability
Set global access and error log file paths.
globals {
log "/var/log/ferron/access.log"
error_log "/var/log/ferron/error.log"
}log "/var/log/ferron/example-access.log" — Override log file per virtual host.
log "/var/log/ferron/example-access.log"log_format "{client_ip} - [{timestamp}] \"{method} {path}\" {status} {bytes_sent}" — Customize the access log format using placeholders.
log_format "{client_ip} {method} {path} {status}"Log to stdout/stderr instead of files. Useful in Docker and containerized environments.
globals {
log "stdout"
error_log "stderr"
}Full Config Example
Production-ready example: globals, security snippet, HTTP→HTTPS redirect, HTTPS vhost with static files + API proxy, rate limiting.
globals {
protocols "h1" "h2"
tls_min_version "TLSv1.2"
ocsp_stapling
log "/var/log/ferron/access.log"
error_log "/var/log/ferron/error.log"
}
snippet "SECURITY_HEADERS" {
header "X-Frame-Options" "DENY"
header "X-Content-Type-Options" "nosniff"
header "Strict-Transport-Security" "max-age=31536000; includeSubDomains"
header_remove "Server"
}
*:80 {
return 301 "https://{header:Host}{path_and_query}"
}
example.com {
tls "/etc/ssl/certs/example.com.crt" "/etc/ssl/private/example.com.key"
root "/var/www/example.com"
use "SECURITY_HEADERS"
etag
compressed
limit rate=200 burst=400
location "/api" remove_base=#true {
proxy "http://localhost:3000"
limit rate=50 burst=100
}
} Conclusion
Ferron shows that a web server need not trade performance for safety or readability: its Rust foundation delivers memory safety without a garbage collector, while the KDL configuration stays legible even across TLS, reverse proxy and PHP setups. With automatic TLS, load balancing and rate limiting built in, many deployments need no extra tooling at all. As the project is still young and evolving quickly, it pays to check the official documentation before relying on it in production.
Further Reading
- Ferron – official website – overview, features and downloads
- Ferron documentation – configuration reference and guides
- ferronweb/ferron – GitHub – source code and issue tracker