Skip to content
Webserver Feature Parity

Web Server Feature Parity: Apache / Nginx / Caddy vs ephpm

Status of ephpm’s built-in HTTP server against the capabilities PHP developers expect from traditional web servers. This page started life as a gap analysis; most of the gap is now closed. What’s below is a snapshot of where we are, followed by the small remaining backlog.


Current ephpm Capabilities

FeatureStatus
Listen on address:portDone (server.listen)
Document rootDone (server.document_root)
Index file fallbackDone (server.index_files)
Static file serving w/ MIME detectionDone (mime_guess crate)
Path traversal protectionDone (canonicalize + boundary check)
PHP routing (.php + clean URLs)Done (extensionless → index.php)
PHP ini overridesDone (php.ini_overrides)
Graceful shutdown (Ctrl+C)Done
URL rewriting / try_files fallbackDone (server.fallback)
TLS / HTTPS (manual + ACME)Done ([server.tls])
Custom error pagesDone (via =404 fallback status codes)
Response headersDone (server.response.headers)
Gzip compressionDone (server.response.compression)
Brotli compressionDone (HTTP responses; preferred over gzip when client supports br)
Timeouts (request, idle, header read)Done (server.timeouts)
Virtual hostsDone (server.sites_dir)
Per-site config overridesDone (drop a site.toml in each vhost directory)
Request size limitsDone (server.request.max_body_size)
Keep-aliveDone (HTTP/1.1 keep-alive with idle timeout)
Rate limitingDone (server.limits.per_ip_rate)
IP connection limitingDone (server.limits.per_ip_max_connections)
ETag / 304 (static + PHP)Done (server.static.etag, server.php_etag_cache)
File cache (metadata + content)Done (server.file_cache)
Blocked paths / securityDone (server.security.blocked_paths, hidden_files)
PHP execution allowlistDone (server.security.allowed_php_paths)
Trusted proxy / X-Forwarded-ForDone (server.security.trusted_proxies)
Host header validationDone (server.request.trusted_hosts)
Prometheus metricsDone (server.metrics)
HTTP/2Done (via ALPN negotiation on TLS connections)
HTTP → HTTPS redirectDone (server.tls.redirect_http — 301 from a separate HTTP listener)
Open file pre-compression (gzip + brotli)Done (server.file_cache.precompress)
open_basedir per vhostDone (server.security.open_basedir)
Disable dangerous PHP funcs per vhostDone (server.security.disable_shell_exec — turns off exec, shell_exec, system, passthru, proc_open, popen, pcntl_exec)

Comparison vs Apache / Nginx / Caddy

How ephpm stacks up against the traditional servers PHP developers reach for. Everything in the “must-have for production PHP hosting” tier is shipping; the remaining gaps are in the optional / nice-to-have tier.

Production must-haves — all shipping in ephpm

FeatureApacheNginxCaddyephpm
URL rewriting / front-controller routingmod_rewriterewrite + try_filesrewrite + try_filesYes (server.fallback)
TLS / HTTPS — manual + automaticmod_ssl (manual)ssl directives (manual)Automatic ACMEYes (manual + ACME, Caddy-style auto-HTTPS)
HTTP → HTTPS redirectRewriteRulereturn 301redirYes (server.tls.redirect_http)
Custom error pagesErrorDocumenterror_pagehandle_errorsYes (via fallback chain — e.g. ["{path}", "/errors/404.html", "=404"])
Response headersmod_headersadd_headerheader directiveYes (server.response.headers)
Gzip compressionmod_deflategzip (built-in)encode (built-in)Yes (server.response.compression)
Brotli compression (HTTP)mod_brotliModulePlugin/built-inYes (preferred over gzip when client supports br)
Pre-compressed static file servingN/Agzip_staticprecompressedYes (server.file_cache.precompress for gzip + brotli)
Access loggingCustomLogaccess_loglog (structured JSON)Yes (server.logging.access)
Timeouts (read header / read body / write / idle)Timeout, KeepAliveclient_body_timeout, etc.read_body, read_header, etc.Yes (server.timeouts)
Virtual hosts<VirtualHost>server blocksSite blocksYes (server.sites_dir, directory-based + lazy discovery)
Per-vhost config overridesper-vhost blockper-server blockper-site blockYes (site.toml in each vhost dir)
Request size limitsLimitRequestBodyclient_max_body_sizerequest_body max_sizeYes (server.request.max_body_size)
Keep-alive tuningKeepAliveTimeoutkeepalive_timeoutidle timeoutYes (server.timeouts.idle)
HTTP/2mod_http2listen … http2AutomaticYes (ALPN negotiation on TLS)
Rate limitingmod_evasive (3rd party)limit_reqPluginYes (server.limits)
Trusted proxy / X-Forwarded-For handlingmod_remoteipreal_ip moduletrusted_proxiesYes (server.security.trusted_proxies)
Path-based access controlsRequire, Denyallow/denypath matcherYes (server.security.blocked_paths, hidden_files)
Multi-tenant PHP isolation (open_basedir, disable shell funcs)per-vhost php_admin_valueper-server fastcgi_paramper-site envYes (server.security.open_basedir + disable_shell_exec)

Optional gaps

FeatureApacheNginxCaddyephpmNotes
Reverse proxymod_proxyproxy_pass + upstreamreverse_proxyNot yetAPI backends, microservices, sidecars
MIME type overridesAddTypetypes blockheader directiveNot yetmime_guess is used; no user overrides yet
IP allow/deny listsRequire ipallow/denyremote_ip matcherNot yetpath-based blocking is covered
Regex [[server.redirects]] (301/302 from regex)mod_rewrite [R]rewrite … permanentredirNot yetSEO migrations / vanity URLs
Regex [[server.rewrites]] with conditionsmod_rewrite RewriteCondrewrite if blocksrewrite matcherNot yetBeyond server.fallback
HTTP/3 (QUIC)Not supportedExperimentalBuilt-inNot yetWould need quinn integration
Basic authmod_auth_basicauth_basicbasic_authNot yetUseful for staging gating
Directory listing / autoindexOptions +Indexesautoindexfile_server browseNot yetLargely replaced by S3-style buckets these days
Zstd compression (HTTP responses)N/AN/APluginNot yetKV store supports zstd; HTTP response negotiation does not

URL Rewriting in ephpm

The piece every PHP framework actually needs — “if the file doesn’t exist on disk, route to the front controller” — is implemented as server.fallback. It’s the direct equivalent of nginx’s try_files $uri $uri/ /index.php?$query_string;.

[server]
listen = "0.0.0.0:8080"
document_root = "/var/www/html"
index_files = ["index.php", "index.html"]

# Try the literal path, then with a trailing slash, then fall back to
# the front controller. Covers WordPress, Laravel, Drupal, Symfony, and
# effectively any framework that routes through index.php.
fallback = ["{path}", "{path}/", "/index.php?{query}"]

Semantics

  • Each entry is checked in order against the filesystem (relative to the document root).
  • {path} = the request URI path; {query} = the original query string.
  • The last entry is treated as an internal rewrite target (not checked against the filesystem) — so a request to /about that doesn’t resolve to a file gets rewritten to /index.php?… internally.
  • Status-code fallbacks are written =404, =403, etc. — handy for “if nothing else matches, return a status.”

Custom error page

Use a fallback entry that points at a static HTML file before the status-code entry:

fallback = ["{path}", "/errors/404.html", "=404"]

When /foo doesn’t resolve and /errors/404.html exists, the HTML is served with a 404 status. If the HTML is missing too, the bare =404 is returned.

Comparison with Apache mod_rewrite

mod_rewrite featureephpm equivalentCovered?
RewriteCond %{REQUEST_FILENAME} !-fimplicit in fallback (filesystem check on each entry)Yes
RewriteCond %{REQUEST_FILENAME} !-dimplicit in fallbackYes
RewriteRule front-controller pattern (. /index.php)last entry of fallbackYes
RewriteRule [QSA] flag{query} placeholder in the fallback targetYes
RewriteRule [L] flagfirst-match-wins is implicitYes
RewriteRule ^foo$ /bar [R=301] (regex 301/302 redirects)regex [[server.redirects]]not yetOptional
RewriteRule with arbitrary RewriteCond (host, header, query)regex [[server.rewrites]] with conditions — not yetOptional
RewriteRule [P] proxy flagrequires reverse proxy — not yetOptional
.htaccess per-directory overridesnot supported, by designNo
RewriteMap external programsnot supportedNo

The “not supported” items are deliberately excluded — chain rules, loops, and per-directory .htaccess files are the source of mod_rewrite’s complexity and security pitfalls; nginx and Caddy don’t replicate them either.

Future: regex-based [[server.redirects]] and [[server.rewrites]]

Some users will want explicit regex redirects (e.g. ^/blog/(\d{4})/(\d{2})/(.+)$ → /articles/$3 with a 301) for SEO migrations or non-standard URL schemes. That’s a separate feature on top of fallbackfallback covers framework routing, [[server.redirects]] would cover URL-rewriting-in-the-Apache-sense. Sketch of the eventual config:

[[server.redirects]]
from = "^/blog/(\\d{4})/(\\d{2})/(.+)$"
to = "/articles/$3"
status = 301

[[server.rewrites]]
from = "^/api/v1/(.+)$"
to = "/api.php?route=$1"
condition = { not_file = true }

Neither blocks production PHP hosting today.