ePHPm β Embedded PHP Manager
Run PHP applications without the infrastructure. No PHP-FPM, no MySQL server, no Redis, no reverse proxy, no certbot. One binary, one config file. Drop in WordPress or Laravel and go. When you need more, it's already built in: MySQL connection pooling, read/write splitting, a Redis-compatible KV store, clustered SQLite with automatic failover, TLS, and Prometheus metrics. One binary from localhost to production β same runtime in development, CI, staging, and prod. No environment drift, no deployment surprises.
Designed by @luthermonson in Arizona π΅ Assembled in Claude Opus 4.6.
Why ePHPm?
How ePHPm compares to other ways of running PHP with a webserver.
| ePHPm | FrankenPHP | RoadRunner | Swoole | Apache + mod_php | Nginx + php-fpm | |
|---|---|---|---|---|---|---|
| Language | Rust | Go (CGO) | Go | PHP + C | C | C |
| Dispatch to PHP | <1 ΞΌs (in-process C call) | ~2β3 ΞΌs (CGO crossings) | ~10β50 ΞΌs (IPC to worker) | <1 ΞΌs (in-process) | <1 ΞΌs (in-process) | ~50β100 ΞΌs (FastCGI socket) |
| Server GC pauses | None | Go GC | Go GC | None | None | None |
| Binary | Single static binary | Caddy module | Go binary + PHP workers | PHP + extension | Apache + modules | Nginx + separate FPM |
| DB proxy + connection pooling | Built-in (MySQL wire, R/W split) | No | No | No | No | No |
| Embedded DB | SQLite via litewire | No | No | No | No | No |
| Built-in KV store | Yes (RESP compatible, in-process) | No | No | No | No | No |
| Query stats (Prometheus) | Built-in | No | No | No | No | No |
| Auto TLS (ACME) | Built-in | Via Caddy | No | No | No | No |
| Clustering | Gossip (SWIM) | No | No | Multi-process (single node) | No | No |
| Virtual hosts | Built-in (directory-based) | Via Caddy | No | No | <VirtualHost> |
server blocks |
| Install size | ~40 MB (varies by PHP extensions) | ~150 MB | ~60β70 MB (rr + PHP) | ~35β45 MB (PHP + .so) | ~50β60 MB (Apache + PHP) | ~40β50 MB (Nginx + PHP) |
| PHP compatibility | Drop-in | Drop-in | Drop-in (worker mode requires PSR-7) | Drop-in (async features require rewrite) | Native (100%) | Native (100%) |
| Deployment | Single binary | Requires Caddy | Multi-process | Requires PHP + Swoole extension | Apache + modules | Separate services |
| Container-friendly | β (single binary) | β (Caddy module) | β | β οΈ (PHP + extension) | β οΈ (heavier) | β οΈ (two services) |
Install
ePHPm is a single self-managing binary β it registers and controls its own system service. There is no install script.
Prefer containers?
docker run -p 8080:8080 ephpm/ephpm:latest. See the Docker section of the install docs for the full tag scheme.
Linux / macOS
Download the latest binary from Releases, unpack, then:
sudo ./ephpm install
This copies the binary to /usr/local/bin/ephpm, writes a default config to /etc/ephpm/ephpm.toml, registers a systemd service (Linux) or launchd plist (macOS), and starts it. The server listens on http://localhost:8080 by default.
Windows
Download ephpm.exe from Releases. In an Administrator PowerShell:
.\ephpm.exe install
Installs to C:\Program Files\ephpm\, adds to PATH, registers a Windows service, and starts it.
Note: Clustered SQLite (sqld) is not available on Windows. Single-node SQLite and the DB proxy work fully.
Manage the service
The same subcommands work on every platform β they wrap systemd / launchd / the Windows service controller:
sudo ephpm start # start the service
sudo ephpm stop # stop
sudo ephpm restart # restart (e.g. after editing the config)
sudo ephpm status # PID, uptime, listen address
sudo ephpm logs # tail the service log (--follow to follow)
Uninstall
sudo ephpm uninstall # remove binary, service, and data dir
sudo ephpm uninstall --keep-data # keep config and SQLite databases
Build from Source
For contributors or custom builds. Requires Rust 1.85+.
# Stub mode (no PHP, fast iteration on HTTP/routing logic)
cargo build
cargo run -- --config ephpm.toml
# Release binary with PHP embedded
# Prerequisites: git, curl, tar, build-essential, pkg-config, libclang-dev, musl-tools (Linux only)
# The PHP SDK (libphp.a + headers) is downloaded from github.com/ephpm/php-sdk releases β
# no PHP CLI, Composer, or static-php-cli toolchain required.
cargo xtask release # β target/release/ephpm
# A locally built binary can self-install too
sudo ./target/release/ephpm install
Configuration
# ephpm.toml
[server]
listen = "0.0.0.0:8080"
document_root = "/var/www/html"
index_files = ["index.php", "index.html"]
[php]
mode = "embedded"
max_execution_time = 30
memory_limit = "128M"
# Load a custom php.ini before applying overrides (optional)
# ini_file = "/etc/php/php.ini"
# INI directive overrides (applied AFTER ini_file)
ini_overrides = [
["display_errors", "Off"],
["error_reporting", "E_ALL"],
]
# Prometheus metrics endpoint
[server.metrics]
enabled = true
# path = "/metrics" # default
# Embedded SQLite (via litewire)
[db.sqlite]
enabled = true
path = "/var/lib/ephpm/app.db"
Any TOML key can be overridden with an EPHPM_ prefixed environment variable β e.g. EPHPM_SERVER__LISTEN=0.0.0.0:9090, EPHPM_PHP__MEMORY_LIMIT=256M. Nesting uses __. Arrays use JSON syntax: EPHPM_CLUSTER__JOIN='["a:7946","b:7946"]'. See Environment Variables for the full mapping rules.
Database: Three Options, Zero Code Changes
ePHPm gives you three database strategies. PHP apps keep their existing pdo_mysql configuration in all cases β no code changes needed.
1. Already have a database? Use the built-in proxy
If you have a MySQL server, ePHPm's DB proxy sits between PHP and your database with connection pooling, read/write splitting, and health checks. PHP connects to localhost:3306 β the proxy handles the rest. PostgreSQL proxy support is planned but not yet implemented.
[db.mysql]
url = "mysql://user:pass@db-server:3306/myapp"
2. Small site? Use embedded SQLite
No external database needed. ePHPm embeds SQLite and exposes it via MySQL wire protocol through litewire. Your PHP app thinks it's talking to MySQL β it's actually talking to SQLite. One binary, one .db file, done.
Back up with cloud volume snapshots (Kubernetes PVCs, EBS snapshots, disk images) or any file-level backup tool.
[db.sqlite]
path = "app.db"
3. Need HA? Use clustered SQLite
For multi-node high availability, ePHPm embeds sqld (Turso's SQLite server) inside the binary. sqld is extracted and spawned as a managed child process at startup β the single-binary model is preserved. Replication happens automatically via WAL frame streaming over gRPC.
- Primary node β accepts writes, streams WAL frames to replicas
- Replica nodes β serve reads locally, forward writes to primary
- Primary election β automatic via ePHPm's gossip layer (lowest-ordinal live node wins)
- Failover β gossip detects failure, next node promotes, sqld restarts in primary mode
[db.sqlite]
path = "/var/lib/ephpm/app.db"
[db.sqlite.replication]
role = "auto"
[cluster]
enabled = true
join = ["ephpm-headless.default.svc.cluster.local"]
How it works under the hood
PHP (pdo_mysql) β litewire (MySQL wire :3306) β SQL Translator β SQLite backend
litewire translates MySQL wire protocol and SQL dialect to SQLite on the fly using sqlparser-rs. It's a standalone open-source project β works outside of ePHPm too.
In single-node mode, the backend is rusqlite (in-process, zero overhead). In clustered mode, it switches to an HTTP client talking to the local sqld instance. Either way, PHP sees a MySQL server at 127.0.0.1:3306.
See docs/architecture/sql.md for the full architecture, failover details, and configuration reference.
KV Store: Three Ways to Use It, Zero External Services
ePHPm ships a DashMap-backed in-process key-value store with TTLs, atomic counters, hashes, LRU eviction, and optional value compression (gzip/zstd/brotli). No Redis server, no extension to install. Like the database, you pick the access pattern β the data lives in the same binary either way.
1. Already use phpredis / predis? Speak RESP
The KV store speaks Redis RESP2 on 127.0.0.1:6379. Existing PHP code using phpredis, predis, or any other Redis client connects unchanged. Commands implemented: GET / SET / SETEX / SETNX / MGET / MSET / INCR / DECR / INCRBY / DECRBY / APPEND / STRLEN / GETSET / DEL / EXISTS / EXPIRE / PEXPIRE / TTL / PTTL / PERSIST / TYPE / RENAME / KEYS / DBSIZE / HSET / HGET / HDEL / HGETALL / HKEYS / HVALS / HLEN / HEXISTS / AUTH / PING / ECHO / INFO.
[kv]
memory_limit = "256MB"
eviction_policy = "allkeys-lru" # noeviction | allkeys-lru | volatile-lru | allkeys-random
compression = "zstd" # none | gzip | brotli | zstd (transparent, per-value)
[kv.redis_compat]
enabled = true # off by default β multi-tenant deployments should keep it off
listen = "127.0.0.1:6379"
2. Want zero round-trips? Use the native PHP functions
Every request gets a set of ephpm_kv_* functions registered as part of the ePHPm SAPI β they call directly into the in-process store with no socket, no protocol parse, no serialization. No extension to install, no client library to configure. In multi-tenant (sites_dir) mode they are automatically namespaced per virtual host β each site sees its own keyspace.
ephpm_kv_set('cart:42', $json, 3600); // value, TTL seconds
$cart = ephpm_kv_get('cart:42');
ephpm_kv_incr_by('views:home', 1);
ephpm_kv_expire('session:abc', 1800);
ephpm_kv_del('cart:42');
Available: ephpm_kv_get, set, del, exists, incr, decr, incr_by, expire, ttl, pttl. These are the recommended API for multi-tenant deployments β the RESP listener has no per-tenant namespace filtering, so leave it disabled and let PHP go through the SAPI bridge.
3. Need HA? Use the clustered KV tier
In a cluster, the KV store becomes a two-tier distributed store with no extra moving parts β it piggybacks on the same SWIM gossip layer used for SQLite primary election.
- Small values (< 1 KB by default) ride the gossip tier β eventually consistent, replicated to every node, sub-millisecond reads everywhere.
- Large values live on a consistent-hash data plane β each key is owned by N nodes (configurable replication factor), fetched on demand via TCP, with optional hot-key promotion that caches frequently-fetched remote values locally.
- Failover β when a node leaves the gossip view, the hash ring rebalances and owned keys migrate to the next replicas. No primary, no election β every node can read and write.
[cluster]
enabled = true
join = ["ephpm-headless.default.svc.cluster.local"]
[cluster.kv]
small_key_threshold = 1024 # bytes β under this, replicate via gossip
replication_factor = 3 # large keys: 3 owners on the ring
replication_mode = "async" # async | sync
hot_key_cache = true # cache hot remote values locally
hot_key_max_memory = "64MB"
Multi-tenant security
When sites_dir is set, each virtual host gets its own isolated keyspace. The ephpm_kv_* PHP functions are namespaced automatically. For the RESP endpoint, ePHPm derives a per-site password from HMAC-SHA256(kv.secret, hostname) and injects it into PHP $_ENV as EPHPM_REDIS_PASSWORD for each request β so phpredis can AUTH without any per-site config in your code.
How it works under the hood
PHP β ephpm_kv_* (in-process function call, ~ns)
PHP β phpredis β :6379 (RESP2) β DashMap store
cluster mode β
gossip tier (small values, eventually consistent)
data plane (large values, consistent-hash, replicated)
The store is a single DashMap<String, Entry> with concurrent reads/writes, async TTL expiry, and an approximate-memory tracker driving eviction. Compression is applied per-value above a size threshold and is transparent on read. In clustered mode the same Store is wrapped with a routing layer that consults the hash ring for non-local keys.
Query Stats & Observability
Every SQL query β whether it goes through the DB proxy to a real MySQL server or through litewire to SQLite β is tracked automatically. ePHPm normalizes queries (replacing literal values with ?), groups them by digest, and records timing, throughput, and error rates.
Metrics are emitted via Prometheus at /metrics:
# Histogram of query execution times, by digest and kind (query/mutation)
ephpm_query_duration_seconds_bucket{digest="SELECT * FROM users WHERE id = ?",kind="query",le="0.01"} 4521
# Total query count by status
ephpm_query_total{digest="SELECT * FROM users WHERE id = ?",kind="query",status="ok"} 4520
ephpm_query_total{digest="SELECT * FROM users WHERE id = ?",kind="query",status="error"} 1
# Rows returned/affected
ephpm_query_rows_total{digest="SELECT * FROM users WHERE id = ?",kind="query"} 4520
# Slow query counter (exceeds threshold)
ephpm_query_slow_total 3
# Active digest count
ephpm_query_active_digests 47
Slow queries (default: > 1s) are logged at WARN level with the normalized SQL and digest ID. Query stats are on by default but fully configurable:
[db.analysis]
query_stats = true # set to false to disable (zero overhead)
slow_query_threshold = "500ms"
Point Grafana, Datadog, or any Prometheus-compatible tool at http://your-ephpm:8080/metrics to chart query latency, throughput, error rates, and identify slow queries β no APM agent or database plugin needed.
See docs/architecture/query-stats.md for the full design.
Virtual Hosts: Multi-Tenant Hosting
Run multiple WordPress sites on a single ePHPm instance. The directory structure IS the config β each subdirectory is named after a domain.
[server]
listen = "0.0.0.0:8080"
document_root = "/var/www/marketing" # fallback for unmatched domains
sites_dir = "/var/www/sites" # vhost directory
/var/www/
marketing/ # signup page (fallback for unknown domains)
sites/
alice-blog.com/ # served when Host: alice-blog.com
index.php
ephpm.db
bobs-recipes.com/ # served when Host: bobs-recipes.com
index.php
ephpm.db
- Add a site: create a directory, drop in WordPress
- Remove a site: delete the directory β traffic falls back to your marketing page
- No per-site config needed: sites inherit global PHP settings, timeouts, and security rules
- Shared thread pool: all sites share tokio's
spawn_blockingpool β 20 blogs don't need 20x the memory
A $3.69/mo Hetzner VM (2 ARM cores, 4 GB RAM) comfortably runs 20 WordPress blogs at ~$0.18/site. See docs/architecture/vhosts.md and docs/architecture/hosting.md for full details.
Project Structure
crates/
βββ ephpm/ CLI binary β clap args, config loading, server boot
βββ ephpm-server/ HTTP server β hyper + tokio, routing, static files, metrics
βββ ephpm-php/ PHP embedding β FFI bindings, SAPI, request/response
βββ ephpm-config/ Configuration β figment, TOML + env var overrides
βββ ephpm-kv/ Embedded KV store β DashMap, RESP2 protocol, TTL/expiry, compression
βββ ephpm-db/ DB proxy β MySQL wire protocol, connection pooling
βββ ephpm-sqld/ sqld embedding β binary extraction, process lifecycle, health checks
βββ ephpm-cluster/ Clustering β SWIM gossip (chitchat), consistent hash ring, SQLite election
Key design decisions:
- Conditional compilation β All PHP FFI code is gated behind
#[cfg(php_linked)]. Stub mode compiles and tests without a PHP SDK. - C wrapper for safety β PHP uses
setjmp/longjmpfor error handling. All RustβPHP calls go throughephpm_wrapper.cwithzend_try/zend_catchguards to prevent stack corruption. - Async I/O, blocking PHP β tokio handles HTTP connections. PHP execution runs on
spawn_blockingthreads (ZTS). - litewire for SQL β wire protocol translation is a separate concern; litewire handles it as a library, ePHPm manages the sqld lifecycle and config.
Contributing
Prerequisites
- Rust 1.85+ β https://rustup.rs (on Windows, also install C++ Build Tools)
- Nightly Rust β
rustup toolchain install nightly(required forcargo +nightly fmt) - cargo-nextest β
cargo install cargo-nextest --locked - cargo-deny β
cargo install cargo-deny --locked - WSL + Ubuntu (Windows only) β needed for
cargo xtask release(see Quick Start above)
See docs/developer/getting-started.md for detailed setup instructions including per-platform Rust installation.
Workflow
Most development uses stub mode β no PHP SDK or container engine needed:
# Build (stub mode)
cargo build
# Run tests (prefer single-crate runs)
cargo nextest run -p ephpm-server
# Lint (must pass with zero warnings)
cargo clippy --workspace --all-targets -- -D warnings
# Format (requires nightly)
cargo +nightly fmt --all
# Dependency audit
cargo deny check
Build & test tooling (xtask)
cargo xtask release # Download PHP SDK + build ephpm binary (release mode)
cargo xtask php-sdk # Download only the prebuilt PHP SDK for the host platform
cargo xtask e2e-install # Download kind, tilt, kubectl to ./bin (no global install)
cargo xtask e2e # Run E2E tests (creates Kind cluster, builds images, tilt ci)
cargo xtask e2e-up # Start E2E dev env (tilt dashboard at localhost:10350)
cargo xtask e2e-down # Tear down Kind cluster
On Windows, release re-invokes itself inside WSL (cross-compiling to musl from native Windows isn't supported). php-sdk is a plain tarball download and works directly on any platform with curl + tar. The PHP SDK is cached at php-sdk/<version>-<os>-<arch>/ β delete that directory to force a re-download.
E2E commands require Podman or Docker. Run cargo xtask e2e-install to download kind/tilt/kubectl to ./bin/ β no global install needed. See docs/developer/testing.md for details.
Code conventions
- Clippy: Pedantic + all warnings denied. Zero warnings policy.
- Formatting: 2024 edition style, grouped imports. Run
cargo +nightly fmt --all. - Error handling:
thiserrorin library crates,anyhowin the binary. Always add.context(). - Logging:
tracingcrate β debug for requests, info for lifecycle, warn/error for problems. - Unsafe code: Safety comment (
// SAFETY:) before everyunsafeblock explaining invariants. - Documentation:
///on public items,//!at module level.
Docs
- Getting started β Prerequisites, building, IDE setup
- Testing strategy β Unit tests, Tilt + Kind E2E, database testing
- E2E test coverage β 170+ tests across single-node and cluster
- Architecture decisions β Language choice, crate design, PHP execution modes
- Implementation guide β Build system, CI, MVP spec
- CLI design β Command structure, UX principles
- Security model β Threat model, FFI safety, trust boundaries
- Clustering β SWIM gossip, consistent hash ring, two-tier KV
- DB proxy β MySQL wire protocol, connection pooling, query analysis
- Kubernetes deployment β Helm chart, StatefulSet, gossip DNS
- Observability β Prometheus metrics, histogram buckets, phased rollout
- Embedded SQL β litewire integration, sqld lifecycle, single-node vs HA
- Competitive analysis β FrankenPHP, RoadRunner, Swoole comparisons
Related Projects
- litewire β MySQL/PG/TDS wire protocol β SQLite translation proxy. Used by ePHPm for embedded SQL, also works standalone.
License
MIT