Installation on NixOS
Ferron 3 ships a Nix flake with two packages and a NixOS module. The module generates ferron.conf from your Nix configuration and runs the server under systemd. You do not edit a config file on the host.
Flake setup#
1. Add the flake input#
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
ferron.url = "github:ferronweb/ferron/3.x";
};
}The 3.x URL follows the release branch. Run nix flake update to move to the latest published release.
2. Expose the packages#
nixpkgs.overlays = [ inputs.ferron.overlays.default ];The overlay adds pkgs.ferron-bin and pkgs.ferron to your package set. The module uses pkgs.ferron-bin as its default package, so you need the overlay unless you set services.ferron.package yourself. Importing the module does not need the overlay.
3. Import the module#
{
imports = [ inputs.ferron.nixosModules.default ];
}Packages#
| Package | Use it when |
|---|---|
ferron-bin | You run a published release. It installs prebuilt binaries, so the install does not need a Rust toolchain. |
ferron | You need a revision without published archives, such as a fork or an unreleased commit. It compiles the workspace from Cargo.lock. |
Both packages install the same six binaries. See File locations.
Select the source build with the package option:
services.ferron.package = inputs.ferron.packages.${pkgs.stdenv.hostPlatform.system}.ferron;The source build compiles without PGO, so it can run slower than the release archives under load. Prebuilt binaries also embed the real build date and git commit. Source builds record 1980-01-01 and an empty commit to keep builds reproducible.
Pin an exact commit with github:ferronweb/ferron/3.x?rev=<sha>. The pinned commit must still contain the nix/ directory.
Minimal configuration#
{
services.ferron = {
enable = true;
hosts."*:80".root = "/var/www/ferron";
};
}Activate the configuration with sudo nixos-rebuild switch --flake .#hostname. Ferron then serves /var/www/ferron on port 80.
If you declare no hosts, the module still adds a *:80 host that serves the demo web root from the package. So enable = true alone gives you a working test page. When you declare your own hosts, the default *:80 host stays. Set hosts."*:80".config yourself to replace its content.
What the module manages#
- Service user: the server runs as the
ferronsystem user and group. Change the names withuserandgroup. - Privileges: the service gets
CAP_NET_BIND_SERVICE, so it binds ports 80 and 443 without root. - Directories: systemd creates
/var/lib/ferronfor state,/var/log/ferronfor logs, and/run/ferronfor runtime files. - Config validation: the unit runs
ferron validateinExecStartPrebefore every start. An invalid config fails the start instead of a running server. - Firewall: the module opens TCP ports 80 and 443 in
networking.firewall. SetopenFirewall = falseif you serve custom ports or manage the firewall yourself. - Recovery: the unit restarts the server after a failure.
systemctl reload ferronsendsSIGHUP, which makes Ferron read its config again.
Host configuration#
services.ferron.hosts maps a Ferron host selector to a set of options. The module writes each attribute name into ferron.conf as it is. Any selector that Ferron understands works, such as *:80, example.com, or *.example.com.
How a host block is generated#
The module writes typed options first, in a fixed order. It appends the verbatim config string last. Ferron resolves duplicate directives per directive. For example, root and index keep the first value, while header keeps the last one.
So a directive in config does not always override the same typed option. Use typed options for the directives they cover, and use config for directives that have no typed option. If you must repeat a typed directive, check its page in the configuration reference first.
This configuration:
services.ferron.hosts."example.com" = {
root = "/var/www/example";
index = [ "index.html" "index.htm" ];
spaFallback = true;
headers = {
set.Content-Security-Policy = "default-src 'self'";
add."X-Client-IP" = "{{remote.ip}}";
unset = [ "Server" ];
};
cache = {
enable = true;
maxResponseSize = 2097152;
};
};generates this host block:
example.com {
root /var/www/example
index index.html index.htm
cache {
max_response_size 2097152
}
header Content-Security-Policy "default-src 'self'"
header +X-Client-IP "{{remote.ip}}"
header -Server
rewrite r"^/.*" "/" {
last
directory false
file false
}
}The fixed order is TLS, HTTPS redirect, root, index, cache, proxy, fcgi_php, headers, and the SPA rewrite.
Static files#
rootsets the directory that the host serves. The defaultnullomits the directive. Paths are written as they are, so useconfigfor paths with spaces.indexlists the files that Ferron tries when a request resolves to a directory. The defaultnullkeeps the Ferron default ofindex.html,index.htm, andindex.xhtml.spaFallbackserves client-side routes. The server rewrites requests that match no file or directory to/. So/dashboard/settingsreturnsindex.htmlinstead of a 404. This option requiresroot.
See Static file serving for the directive details.
Reverse proxy#
proxysets one upstream URL. The module writes theproxy <url>shorthand.proxyExtraConfigplaces lines inside aproxy { ... }block instead of the shorthand. The default empty string renders the one-line form.
services.ferron.hosts."app.example.com" = {
proxy = "http://127.0.0.1:3000";
proxyExtraConfig = "algorithm round_robin";
};Use config when you need several upstreams, because the typed proxy option takes one URL. See Reverse proxying for the nested options.
PHP#
fcgiPhp sets a PHP-FPM backend as tcp://host:port or unix:///run/php/php-fpm.sock. Pair it with root so that Ferron also serves static files. See FastCGI and CGI for details.
TLS#
tls.enabledefaults totrue, which writes no directive and keeps the Ferron automatic TLS behavior. Set it tofalseto writetls falseand serve plain HTTP only.tls.certandtls.keyset a certificate and a private key. The module writes them astls <cert> <key>. Set both or neither.httpsRedirectacceptstrueorfalseand writeshttps_redirect. The defaultnullomits the directive, so Ferron keeps redirecting to HTTPS when TLS is active.
The module passes values as they are, so {{env.VAR}} interpolation works in paths. See TLS for certificate formats and providers.
Response headers#
headers.setreplaces a response header. The module writesheader <Name> "<value>".headers.addappends a header and keeps existing values. The module writesheader +<Name> "<value>". Use it for headers that may repeat.headers.unsetis a list of header names to remove. The module writesheader -<Name>for each name.
The module adds the quotes, so values with spaces or semicolons need no escaping. It escapes backslashes and double quotes. Placeholders such as {{remote.ip}} pass through, and Ferron fills them per request. See HTTP headers and CORS for details.
Response caching#
cache.enableturns on the in-memory response cache for the host. The module writes the barecachedirective. The default isfalse. To disable caching again for a sub-path, putcache falseinconfig.cache.maxResponseSizesets the largest response body that the server stores, in bytes. The defaultnullkeeps the Ferron default of 2 MiB. Ferron still serves larger responses.cache.varylists request headers that the server adds to the cache key.cache.varyCookieslists cookie names that the server adds to the cache key.cache.ignorelists response headers that the server drops from the stored copy.cache.litespeedOverrideCacheControlandcache.emitLitespeedHeaderssupport applications that expect LSCache response headers.
See HTTP cache for all cache directives.
Option reference#
| Option | Default | Writes |
|---|---|---|
root | null | root <path> |
index | null | index <name>... |
spaFallback | false | Rewrite to / for unmatched requests |
proxy | null | proxy <url> |
proxyExtraConfig | "" | Lines inside the proxy { ... } block |
fcgiPhp | null | fcgi_php <url> |
tls.enable | true | Nothing, or tls false when set to false |
tls.cert, tls.key | null | tls <cert> <key> |
httpsRedirect | null | https_redirect true or false |
headers.set | {} | header <Name> "<value>" |
headers.add | {} | header +<Name> "<value>" |
headers.unset | [] | header -<Name> |
cache.enable | false | cache |
cache.maxResponseSize | null | max_response_size <bytes> |
cache.vary, cache.varyCookies | [] | vary ..., vary_cookies ... |
cache.ignore | [] | ignore ... |
cache.litespeedOverrideCacheControl | false | litespeed_override_cache_control |
cache.emitLitespeedHeaders | false | emit_litespeed_headers |
Escape hatches#
hosts."<selector>".configholds raw lines for that host. The module appends them after the typed directives. Use them for directives that have no typed option.extraConfigholds raw text that the module appends after all host blocks. Use it for matchers and other top-level directives.configFilepoints at a config file that you manage. When it is set, the module ignoresglobalConfig,hosts, andextraConfig. It also adds no default host.
services.ferron.configFile = pkgs.writeText "ferron.conf" ''
example.com {
root /var/www/example
}
'';Global configuration#
globalConfig holds the contents of the global { ... } block. The default sets access log and error log rotation. extraConfig appends text after the host blocks.
services.ferron = {
globalConfig = ''
log /var/log/ferron/access.log {
access_log_rotate_size 10485760
access_log_rotate_keep 7
}
'';
};See Configuration syntax for the global block directives.
Module options#
| Option | Default | Purpose |
|---|---|---|
enable | false | Runs the server and opens the firewall ports. |
package | pkgs.ferron-bin | Binary that the service runs. |
user | "ferron" | System user for the service. |
group | "ferron" | System group for the service. |
hosts | {} | Host blocks, keyed by selector. Adds a default *:80 demo host. |
globalConfig | Log rotation | Contents of the global { ... } block. |
extraConfig | "" | Raw text that the module appends after the host blocks. |
configFile | null | Uses your own config file and ignores the settings above. |
openFirewall | true | Opens TCP ports 80 and 443 in networking.firewall. |
File locations#
/nix/store/.../bin/: theferronserver, plusferron-fmt,ferron-kdl2ferron,ferron-passwd,ferron-precompress, andferron-serve./nix/store/.../share/ferron/ferron.conf.example: example config from the upstream package./nix/store/.../share/ferron/wwwroot/: demo web root, used by the default*:80host./var/log/ferron/access.logand/var/log/ferron/error.log: server logs./var/lib/ferronand/run/ferron: state and runtime files.
The generated config lives in the Nix store. Run sudo systemctl cat ferron to see the exact path that the server reads.
Managing the service#
sudo systemctl status ferron # process state and recent log lines
sudo systemctl reload ferron # read the config again
sudo systemctl restart ferron # stop and start the server
sudo journalctl -u ferron.service # all service log entriesKeeping versions current#
nix/package-bin.nix pins the release version and the download hashes. nix/package.nix pins the two git submodules that the build uses.