simple-httpd reads INI, YAML (.yml / .yaml), or JSON. --config chooses the parser from the file path. Unknown keys are ignored.
INI section names other than [vhost:hostname] are ignored; keys are global. YAML and JSON use a virtual_hosts array instead. Comments: # / ; in INI, # in YAML. JSON has no comments.
Precedence, later wins:
- Compiled defaults
- Config file (
--config,SIMPLE_HTTPD_CONFIG, or a well-known path) - Environment (
SIMPLE_HTTPD_*) - Command-line flags
./build/simple-httpd --config config/examples/simple.conf.example --dump-config
./build/simple-httpd --config config/examples/simple.yml.example --dump-config
./build/simple-httpd --config config/examples/simple.json.example --dump-config--dump-config prints the resolved values after file + env + CLI. Use it before starting a production listener.
Copy a file from config/examples/ (.conf.example, .yml.example, or .json.example), edit paths, then pass --config.
| File | Use when |
|---|---|
| simple (yml, json) | Local HTTP, ./www, logs on the terminal |
| advanced (yml, json) | Several Host names, gzip, cache, optional TLS |
| production (yml, json) | HTTPS on 443, file logs, rate limits, vhosts |
| security (yml, json) | Tight timeouts, TLS + HSTS + CSP, method filter |
| htpasswd.example | user:password lines for auth_basic_file |
config/simple-httpd.conf.example matches the simple profile. See config/README.md.
TLS starts only when both ssl_cert and ssl_key are set. The files must exist by the time start() runs.
| Key | Default | Meaning |
|---|---|---|
listen_address |
0.0.0.0 |
Bind address (127.0.0.1, ::, …) |
listen_port |
8080 |
Bind port. 0 means ephemeral (tests) |
document_root |
./www |
Default site root |
index_files |
index.html, index.htm |
Tried in order for a directory |
directory_listing |
false |
HTML autoindex when no index file exists |
follow_symlinks |
false |
If false, a symlink target is refused |
server_name |
simple-httpd/<version> |
Server response header |
Virtual hosts: a section [vhost:app.example] with document_root = …. The name is matched against the request Host header (port stripped, case-insensitive). Unknown hosts use the default document_root.
document_root = /var/www/html
[vhost:app.example]
document_root = /var/www/app.exampleYAML / JSON use a list instead:
virtual_hosts:
- name: app.example
document_root: /var/www/app.example"virtual_hosts": [
{ "name": "app.example", "document_root": "/var/www/app.example" }
]The process still has one listen socket. HTTPS, if enabled, applies to that socket for every host. Per-vhost certificates are not supported.
| Key | Default | Meaning |
|---|---|---|
worker_threads |
4 |
Size of the connection worker pool |
http2 |
true |
Speak HTTP/2 (h2c prior knowledge, TLS ALPN h2) when the build has nghttp2 |
max_connections |
128 |
Concurrent accepted connections |
request_timeout_seconds |
30 |
Read timeout while parsing a request |
keep_alive |
true |
Honor HTTP/1.1 persistent connections |
keep_alive_timeout_seconds |
5 |
Idle timeout between keep-alive requests |
max_request_size |
65536 |
Max bytes for request line + headers + body |
max_header_count |
100 |
Max header fields |
POST / PUT bodies are parsed when framed with Content-Length or Transfer-Encoding: chunked. Static files still hit the method allow-list (default: 405). Proxy, CGI, and FastCGI skip the allow-list so backends can accept POST.
| Key | Default | Meaning |
|---|---|---|
ssl_cert / tls_cert / cert |
empty | PEM certificate chain |
ssl_key / tls_key / key |
empty | PEM private key |
hsts |
false |
Send Strict-Transport-Security (HTTPS only) |
hsts_max_age |
31536000 |
HSTS max-age in seconds |
CLI: --tls-cert, --tls-key, --hsts. Env: SIMPLE_HTTPD_TLS_CERT, SIMPLE_HTTPD_TLS_KEY.
Protocol floor is TLS 1.2. Cipher selection is OpenSSL's server defaults for this build. When HTTP/2 is enabled, ALPN prefers h2 then http/1.1. The server SSL session cache is on.
| Key | Default | Meaning |
|---|---|---|
compression / gzip |
true |
Compress when zlib is linked and the client sends Accept-Encoding |
compression_min_length |
256 |
Skip compression below this body size |
cache_max_age |
0 |
If > 0, send Cache-Control: public, max-age=N |
file_cache |
true |
Keep small static bodies in memory |
file_cache_max_bytes |
2097152 |
Cap for the whole cache |
file_cache_max_entry |
262144 |
Skip files larger than this |
file_cache_ttl_seconds |
60 |
Drop an entry after this many seconds |
mime_type / mime_types |
empty | Extra mappings .ext:type (comma-separated) |
--no-compression turns compression off. gzip is preferred when the client offers it; otherwise deflate. Types that compress are the usual text / JSON / JS / SVG set; already-compressed formats are left alone. The file cache keys on path + size + mtime and is skipped for range responses.
mime_type = .wasm:application/wasm, .foo:text/x-fooThese are off or unused unless you set them. The product is still a static file server; CGI, FastCGI, SSI, and reverse proxy are extra surface area.
| Key | Default | Meaning |
|---|---|---|
ssi |
false |
Expand <!--#include --> / <!--#echo --> in HTML |
cgi_prefix |
empty | URL prefix whose files are executed as CGI |
cgi_timeout_seconds |
10 |
Kill CGI / FastCGI after this many seconds |
fastcgi |
empty | Prefix → TCP FastCGI backend (/app:127.0.0.1:9000) |
proxy |
empty | Prefix → HTTP backends (/api:127.0.0.1:9000,127.0.0.1:9001) |
proxy_timeout_seconds |
30 |
Upstream read/connect timeout |
proxy_max_body |
1048576 |
Max proxied response body |
proxy = /api:127.0.0.1:9000,127.0.0.1:9001; /other:10.0.0.2:80Comma-separated hosts on one rule are round-robin. A connect failure tries the next backend. Several rules are separated with ;. YAML/JSON may use a list of the same strings (do not join them with commas — that would smash backends together). Backends are HTTP only (http:// optional). https:// origins are rejected. Hop-by-hop headers are stripped. WebSocket Upgrade is tunneled on HTTP/1.1 only (not HTTP/2).
cgi_prefix = /cgi-bin
allow_methods = GET, HEAD, OPTIONS, POSTThe path under the prefix must map to an executable file inside the document root. The daemon forks (POSIX) or CreateProcess (Windows), sets a CGI environment, and parses Status / Content-Type from the script's stdout. <!--#exec --> in SSI is not CGI.
fastcgi = /app:127.0.0.1:9000Minimal FastCGI client over TCP. There is no unix-socket helper, no php-fpm supervisor, and no connection pool. Run the application process yourself.
ssi = trueHTML (text/html) responses expand:
<!--#include virtual="/frag.html" -->or<!--#include file="frag.html" -->(confined to the document root, depth 3)<!--#echo var="DATE_GMT" -->,DOCUMENT_URI,DOCUMENT_NAME,QUERY_STRING
<!--#exec --> is refused. There is no other template engine.
/healthz, /metrics, and /status are the ops handlers. Optional auth-gated {admin_path}/config and {admin_path}/reload are the management surface. There is no GraphQL or application plugin router.
| Key | Default | Meaning |
|---|---|---|
security_headers |
true |
X-Frame-Options, Referrer-Policy, Permissions-Policy |
x_frame_options |
DENY |
Value for X-Frame-Options |
referrer_policy |
strict-origin-when-cross-origin |
Referrer-Policy |
csp |
empty | Content-Security-Policy; omitted when empty |
allow_methods |
GET, HEAD, OPTIONS |
Methods that reach static files |
auth_basic_realm |
simple-httpd |
WWW-Authenticate realm |
auth_basic_file |
empty | user:password file; empty disables auth |
rate_limit_enabled |
false |
Per-IP request window |
rate_limit_requests |
120 |
Max requests per window |
rate_limit_window_seconds |
60 |
Window length |
allow_ips |
empty | Comma-separated CIDRs; empty = all (after deny) |
deny_ips |
empty | Comma-separated CIDRs; deny wins |
max_connections_per_ip |
0 |
Concurrent connections per IP (0 = unlimited) |
connection_rate_limit_enabled |
false |
Cap new accepts per IP |
connection_rate_limit |
60 |
Max new connections per window |
connection_rate_window_seconds |
60 |
Connection-rate window |
security_log |
empty | Security audit file (- or empty → stderr with errors) |
tls_min_version |
1.2 |
1.2 or 1.3 (floor; 1.3 preferred when available) |
admin_path |
empty | Base path for management API (requires auth_basic_file) |
user / group |
empty | Drop privileges after bind (Unix) |
daemonize |
false |
Double-fork to background (Unix; prefer systemd) |
pid_file |
empty | Write PID after bind |
X-Content-Type-Options: nosniff is always sent on responses from the static handler.
Directory listing injects inline CSS. A CSP of default-src 'self' will blank the listing styles unless you add style-src 'self' 'unsafe-inline' (the security profile does this).
Auth file format: one user:password per line, # comments, no hashing. chmod 600. If the file is set but missing, unreadable, or empty of users, the server refuses to start.
/healthz, /metrics, and /status skip auth and request rate limits. Admin endpoints never skip Basic auth.
Rewrite:
rewrite = /blog:/posts, /old:/index.htmlPrefix match: /blog becomes /posts; /blog/a becomes /posts/a. A longer path that only shares a prefix (/blogging) is not rewritten. First matching rule wins.
| Key | Default | Meaning |
|---|---|---|
health_path |
/healthz |
Plain-text ok\n (empty string disables) |
metrics_path |
/metrics |
Prometheus text (empty string disables) |
status_path |
/status |
JSON process snapshot (empty string disables) |
admin_path |
empty | Management base path; empty disables |
tcp_nodelay |
true |
Set TCP_NODELAY on accepted sockets |
sendfile |
true |
Prefer kernel sendfile for cleartext static bodies |
/healthz, /metrics, and /status skip auth and request rate limits. When admin_path is set (e.g. /admin), these require Basic auth (auth_basic_file mandatory):
| Method | Path | Action |
|---|---|---|
GET |
{admin_path}/config |
Dump resolved configuration (plaintext) |
POST |
{admin_path}/reload |
Request a soft reload (same as SIGHUP) |
| Key | Default | Meaning |
|---|---|---|
log_level |
info |
debug, info, warn, error |
access_log |
- |
Path, or - for stdout |
error_log |
- |
Path, or - for stderr |
security_log |
empty | Security events; empty/- shares stderr with errors |
Access lines look like Combined Log Format with RFC 1123 dates in GMT. Security lines are TIMESTAMP SECURITY event=… detail….
See the README table. Flags only override a key when that flag is actually passed. An empty CLI does not reset access_log or TLS paths loaded from the file.
| Variable | Sets |
|---|---|
SIMPLE_HTTPD_CONFIG |
Default config path when --config is omitted |
SIMPLE_HTTPD_ADDRESS |
listen_address |
SIMPLE_HTTPD_PORT |
listen_port |
SIMPLE_HTTPD_ROOT |
document_root |
SIMPLE_HTTPD_LOG_LEVEL |
log_level |
SIMPLE_HTTPD_TLS_CERT |
ssl_cert |
SIMPLE_HTTPD_TLS_KEY |
ssl_key |
validate() fails when:
listen_addressordocument_rootis emptyworker_threadsormax_connectionsis 0max_request_sizeis under 1024index_filesorallow_methodsis empty- only one of
ssl_cert/ssl_keyis set - TLS is configured in a build compiled without OpenSSL
- a virtual host is missing
nameordocument_root - rate limiting is on and
rate_limit_requestsis 0 - connection rate limiting is on and
connection_rate_limitis 0 allow_ips/deny_ipsis set but has no valid CIDR entriestls_min_versionis not1.2/1.3admin_pathis set withoutauth_basic_file, or does not start with/
At process start, the default document root and every vhost root must exist as filesystem paths.