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 ferron

Default 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:2

Service Management (systemctl)

systemctl start ferron — Start the Ferron web server.

systemctl start ferron

systemctl stop ferron — Stop the Ferron web server.

systemctl stop ferron

systemctl restart ferron — Restart Ferron (briefly drops connections).

systemctl restart ferron

systemctl reload ferron — Reload the Ferron configuration with zero downtime. Re-reads /etc/ferron.kdl.

systemctl reload ferron

systemctl status ferron — Show Ferron service status, PID, and recent log output.

systemctl status ferron

systemctl enable ferron — Enable Ferron to start automatically on system boot.

systemctl enable ferron

systemctl disable ferron — Disable Ferron from starting automatically on boot.

systemctl disable ferron

CLI Commands

ferron — Start Ferron using ferron.kdl in the current directory.

ferron

ferron -c /etc/ferron.kdl — Start Ferron with a specific configuration file. Default: ./ferron.kdl

ferron -c /etc/ferron.kdl

ferron --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.yaml

ferron --module-config — Display the compiled-in module configuration and available modules.

ferron --module-config

ferron -V — Show the Ferron version.

ferron -V

ferron serve — Quickly serve the current directory over HTTP on port 3000 (no config file needed).

ferron serve

ferron serve -p 8080 -r /var/www/html — Serve a specific directory on a custom port.

ferron serve -p 8080 -r /var/www/html

ferron 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/www

ferron 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 mysecretpassword

ferron-precompress /var/www/assets — Pre-compress static assets (gzip/brotli) using 64 threads for faster delivery.

ferron-precompress /var/www/assets

ferron-precompress -t 8 /var/www/assets — Pre-compress assets with a specific number of parallel threads.

ferron-precompress -t 8 /var/www/assets

ferron-yaml2kdl ferron.yaml ferron.kdl — Migrate a legacy Ferron 1.x YAML config to the modern KDL format.

ferron-yaml2kdl old-config.yaml ferron.kdl

KDL 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 domains

KDL 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 values

Location 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).

compressed

directory_listing #true — Enable directory listing (disabled by default).

directory_listing #true

file_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_keepalive

proxy_http2_only — Force HTTP/2-only proxying to the backend. Required for gRPC backends.

proxy_http2_only

proxy_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=#true

rewrite "^/(.*)$" "/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=#true

Single-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_for

Rate 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 https

Logging & 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

  • apache – established, modular web server with a large ecosystem
  • caddy – Go web server with automatic TLS and simple configuration
  • certbot – issue and renew Let's Encrypt certificates manually