RepoPilotOpen in app →

php/frankenphp

🧟 The modern PHP app server

Healthy

Healthy across the board

weakest axis
Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit today
  • 19 active contributors
  • Distributed ownership (top contributor 41% of recent commits)
Show all 6 evidence items →
  • MIT licensed
  • CI configured
  • Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/php/frankenphp)](https://repopilot.app/r/php/frankenphp)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/php/frankenphp on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: php/frankenphp

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/php/frankenphp shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across the board

  • Last commit today
  • 19 active contributors
  • Distributed ownership (top contributor 41% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live php/frankenphp repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/php/frankenphp.

What it runs against: a local clone of php/frankenphp — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in php/frankenphp | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch main exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 30 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>php/frankenphp</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of php/frankenphp. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/php/frankenphp.git
#   cd frankenphp
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of php/frankenphp and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "php/frankenphp(\\.git)?\\b" \\
  && ok "origin remote is php/frankenphp" \\
  || miss "origin remote is not php/frankenphp (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify main >/dev/null 2>&1 \\
  && ok "default branch main exists" \\
  || miss "default branch main no longer exists"

# 4. Critical files exist
test -f "cgo.go" \\
  && ok "cgo.go" \\
  || miss "missing critical file: cgo.go"
test -f "caddy/caddy.go" \\
  && ok "caddy/caddy.go" \\
  || miss "missing critical file: caddy/caddy.go"
test -f "caddy/php-server.go" \\
  && ok "caddy/php-server.go" \\
  || miss "missing critical file: caddy/php-server.go"
test -f "context.go" \\
  && ok "context.go" \\
  || miss "missing critical file: context.go"
test -f "cli.go" \\
  && ok "cli.go" \\
  || miss "missing critical file: cli.go"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 30 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~0d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/php/frankenphp"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

FrankenPHP is a modern application server for PHP written in Go that embeds PHP via CGI into the Caddy web server. It solves the PHP deployment problem by providing a single statically-compiled binary with worker mode, Early Hints, HTTP/2, HTTP/3, automatic HTTPS, hot reloading, and Mercure real-time capabilities—eliminating the need for separate PHP-FPM + nginx setups. Hybrid monorepo: caddy/ subdirectory contains the Caddy module integration (caddy/app.go orchestrates PHP lifecycle, caddy/hotreload.go implements hot reload), build-static.sh handles static compilation, root-level Go files handle the core FFI binding to libphp, Dockerfile and alpine.Dockerfile define container images. PHP code lives under caddy/frankenphp/ with Caddyfile configuration.

👥Who it's for

PHP developers (Laravel, Symfony, and vanilla PHP projects) who want to modernize their deployment architecture without refactoring their codebase. Infrastructure engineers and DevOps teams seeking simpler, faster PHP deployments with automatic HTTPS and performance features built-in.

🌱Maturity & risk

Production-ready and actively developed. The codebase shows extensive test coverage (.github/workflows/tests.yaml, *_test.go files throughout), proper CI/CD pipelines for Docker, Windows, and sanitizers, and recent Go 1.26.0 adoption. Multiple stable releases exist (v1.12.2 current, semantic versioning in use), indicating mature project governance.

Low-to-moderate risk for core users, moderate complexity for contributors. The project has substantial Go and C dependencies (Caddy v2.11.2, certmagic, libphp compilation), but binaries are statically linked (low runtime risk). Single primary maintainer (dunglas) is visible in go.mod replace directives and GitHub workflows. Breaking changes are tracked (v1.0.0-rc.1 retracted). Risk increases if you need custom PHP extensions not in the static build.

Active areas of work

Active maintenance on PHP 8.5 support (evident from README), recent additions of sanitizer workflows (.github/workflows/sanitizers.yaml), hot reload feature development (caddy/hotreload.go, caddy/hotreload_test.go), Windows support (windows.yaml workflow), and dependency updates (Caddy v2.11.2, Go 1.26.0). Mercure real-time integration is a current focus (multiple mercure imports in go.mod).

🚀Get running

git clone https://github.com/dunglas/frankenphp.git
cd frankenphp
# Install Go 1.26.0+, then build the Caddy integration:
cd caddy
go mod download
go build -o frankenphp ./frankenphp
# Or use prebuilt binaries from releases

Daily commands:

# Using prebuilt binary:
./frankenphp run
# With Caddyfile configuration:
cd caddy/frankenphp
../frankenphp run
# Docker:
docker build -f Dockerfile -t frankenphp .
docker run -p 80:80 -p 443:443 frankenphp
# Static build from source:
bash build-static.sh

🗺️Map of the codebase

  • cgo.go — Core CGO bindings that interface PHP runtime with Go—essential for understanding how FrankenPHP embeds PHP.
  • caddy/caddy.go — Main Caddy module integration point where FrankenPHP hooks into Caddy's request handling pipeline.
  • caddy/php-server.go — HTTP request handler for PHP—processes incoming requests and manages PHP worker lifecycle.
  • context.go — Request context management binding HTTP requests to PHP execution—critical for request/response handling.
  • cli.go — CLI entry point for FrankenPHP standalone server—shows how workers are spawned and lifecycle managed.
  • caddy/module.go — Caddy module provisioning and configuration unmarshalling—how FrankenPHP is loaded as a Caddy plugin.
  • caddy/go.mod — Caddy module dependencies showing core integrations with Mercure, Brotli, and Caddy server framework.

🧩Components & responsibilities

  • Caddy HTTP Server (caddy/caddy.go, caddy/module.go) (Go net/http, Caddy framework) — Accepts and routes HTTP requests; applies middleware (compression, TLS, Early Hints); delegates to PHP handler.
    • Failure mode: If Caddy fails, entire server is down; no fallback.

🛠️How to make changes

Add a new Caddy handler directive

  1. Define a new struct with your handler configuration fields in caddy/module.go (caddy/module.go)
  2. Implement CaddyModule() and Provision() methods for Caddy lifecycle (caddy/module.go)
  3. Add ServeHTTP(w http.ResponseWriter, r *http.Request) error method as the handler entry point (caddy/module.go)
  4. Register the module in caddy/extinit.go init() function with caddy.RegisterModule() (caddy/extinit.go)
  5. Write tests in caddy/module_test.go using config unmarshalling patterns (caddy/module_test.go)

Add PHP worker configuration support

  1. Add new config fields to WorkerConfig struct in caddy/workerconfig.go (caddy/workerconfig.go)
  2. Update the Caddy config unmarshalling in caddy/module.go to parse new fields into WorkerConfig (caddy/module.go)
  3. Modify worker pool initialization in cli.go or caddy/php-server.go to use the new config (cli.go)
  4. Add integration tests in caddy/caddy_test.go simulating the new config scenario (caddy/caddy_test.go)

Integrate a new real-time or optimization feature

  1. Create a new file (e.g., caddy/myfeature.go) with feature struct and ServeHTTP middleware chain (caddy/module.go)
  2. Wrap the core PHP handler in caddy/php-server.go with your middleware using http.Handler (caddy/php-server.go)
  3. Register module in caddy/extinit.go and add Caddyfile directive name (caddy/extinit.go)
  4. Document in caddy/frankenphp/Caddyfile with example syntax and defaults (caddy/frankenphp/Caddyfile)

🔧Why these technologies

  • Go + CGO — Enables embedding PHP runtime directly in a compiled binary while leveraging Go's concurrency (goroutines) for request handling and worker management.
  • Caddy web server — Provides production-grade HTTP/1.1, HTTP/2, HTTP/3, TLS automation, and plugin architecture; FrankenPHP extends it as a Caddy module rather than replacing it.
  • Worker mode (goroutine-per-request) — Keeps PHP application state warm between requests, enabling frameworks like Laravel and Symfony to cache bootstrapped instances—major performance win over CGI.
  • Mercure protocol integration — Adds real-time push capabilities (Server-Sent Events over HTTP/2) for live updates without WebSocket complexity.
  • Early Hints (HTTP 103) — Allows sending Link headers before full response, enabling browsers to preload critical assets in parallel with PHP execution.

⚖️Trade-offs already made

  • Embed PHP in a Go binary via CGO rather than fork PHP-FPM processes

    • Why: Eliminates inter-process communication overhead, shared memory, and socket setup; keeps request state in memory for worker mode.
    • Consequence: Tightly couples Go and PHP runtimes; CGO adds build complexity; crashes in PHP can crash the whole server (no isolation).
  • Single Caddy server instance (no master/worker fork model)

    • Why: Simpler architecture; leverages Caddy's built-in graceful reload and admin API; natural fit for containerized/cloud deployments.
    • Consequence: All CPU cores are utilized by goroutines, not separate OS processes; may require external orchestration for rolling updates.
  • Require CLI-based configuration (Caddyfile) rather than pure API-first

    • Why: Familiar to DevOps teams; Caddy ecosystem tools and plugins expect Caddyfile syntax; easier local development.
    • Consequence: Configuration changes require reload (albeit zero-downtime via admin API); less dynamic than runtime config servers.
  • PHP worker pool per FrankenPHP instance (no shared pool across machines)

    • Why: Simplifies request affinity and request-local state; no need for distributed session backend.
    • Consequence: Horizontal scaling requires load balancing; session data must be externalized (Redis) to survive server restarts.

🚫Non-goals (don't propose these)

  • Multi-threaded request handling in the same PHP process (one request per worker at a time).
  • Full backward compatibility with all PHP extensions (some low-level extensions expecting FPM semantics may not work).
  • Windows support with full feature parity (worker mode, some extensions have platform limitations).
  • Replacing Kubernetes-native PHP deployment patterns (FrankenPHP is a single-binary app server, not a cluster orchestrator).

🪤Traps & gotchas

  1. Static PHP build complexity: build-static.sh requires specific system libraries (liboniguruma-dev, libargon2-dev, etc.) and uses Docker; local builds may fail without exact environment setup. 2. PHP version pinning: caddy/go.mod uses replace github.com/dunglas/frankenphp => ../ which points to the root codebase; version mismatches between root and caddy/go.mod can cause silent build failures. 3. Caddyfile syntax: FrankenPHP directives live inside a custom 'frankenphp' block in Caddyfile (caddy/frankenphp/Caddyfile); typos fail silently without schema validation. 4. Worker mode state isolation: persistent PHP processes in worker mode share global state; developers must understand that static variables and $_SERVER globals persist across requests. 5. CGI environment: PHP runs via CGI bridge (caddy/app.go), not FastCGI; some env vars or streaming behaviors differ from traditional setups.

🏗️Architecture

💡Concepts to learn

  • CGI and Server Gateway Interface — FrankenPHP bridges Go HTTP requests to PHP via CGI (not FastCGI); understanding CGI environment variables, stdin/stdout piping, and lifecycle is critical for debugging request handling in caddy/app.go
  • Worker Mode / Process Pooling — Core feature that keeps PHP processes alive across requests; essential for Laravel/Symfony integration and performance, implemented in caddy/app.go's request loop
  • Early Hints (HTTP 103) — FrankenPHP's signature performance feature using HTTP status 103 to push resources before PHP finishes; requires understanding header transmission timing and client support
  • Hot Module Reloading / File Watching — caddy/hotreload.go watches filesystem changes and reloads PHP processes without restarting; understanding fsnotify and graceful shutdown is needed for development improvements
  • Caddy Modules and Hooks — FrankenPHP plugs into Caddy via UnmarshalCaddyfile and module registration (extinit.go); understanding Caddy's lifecycle hooks (OnLoad, OnNew) is required for extensions
  • Static Binary Compilation and musl libc — build-static.sh produces statically-linked binaries without glibc; requires knowledge of CGO_ENABLED, C linking flags, and Alpine Linux for reproducible, distributable artifacts
  • Mercure Protocol — Server-Sent Events protocol integrated into FrankenPHP for real-time features; understanding pub/sub subscription, JWT authorization, and SSE streaming is needed for real-time apps
  • caddyserver/caddy — FrankenPHP is built as a Caddy module; understanding Caddy's architecture is essential for extending FrankenPHP
  • laravel/octane — Laravel's official worker mode integration for FrankenPHP; shows how frameworks integrate with FrankenPHP's request pooling
  • symfony/runtime — Symfony's runtime abstraction for worker mode; demonstrates how Symfony apps bootstrap in FrankenPHP's persistent worker processes
  • dunglas/mercure — Real-time protocol integrated into FrankenPHP (imported in go.mod); provides pub/sub for Early Hints and live data features
  • staticphp/installer — Companion project that builds static PHP binaries independently; used by FrankenPHP's build-static.sh for reproducible compilation

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add integration tests for Caddy module configuration and HTTP/3 support

The repo has extensive Caddy integration code (caddy/module.go, caddy/config_test.go, caddy/caddy_test.go) but missing comprehensive tests for the full HTTP/3 stack and module configuration edge cases. The README prominently features 'HTTP/3 support' as a key feature, but there's no dedicated test coverage for this. This would increase confidence in the HTTP protocol implementations and catch regressions early.

  • [ ] Create caddy/http3_test.go with tests for QUIC configuration and HTTP/3 negotiation
  • [ ] Add tests in caddy/config_test.go for Early Hints, Mercure, and Vulcain module combinations
  • [ ] Test worker mode configuration edge cases in caddy/workerconfig.go via dedicated test file
  • [ ] Add integration tests validating all three modules (Mercure, Vulcain, cbrotli) can coexist

Add GitHub Actions workflow to test Windows builds and CGI compatibility

The repo has .github/workflows/windows.yaml but it's not clear what it tests. The codebase has cgi.go and cgi_test.go suggesting Windows/CGI is a supported feature, but Windows compatibility isn't covered in the main tests.yaml workflow. Adding a dedicated Windows build validation workflow would catch platform-specific regressions early and ensure the CGI integration works on Windows.

  • [ ] Extend .github/workflows/windows.yaml to run cgi_test.go with Windows-specific environment variables
  • [ ] Add test steps for FastCGI protocol compliance on Windows (test TCP listener setup in cgi.go)
  • [ ] Validate Dockerfile builds and basic HTTP functionality on Windows Container images
  • [ ] Document in CONTRIBUTING.md the Windows testing process for local contributors

Add hot-reload functionality tests and watcher robustness coverage

The repo has caddy/hotreload.go and caddy/watcher_test.go, but the test file appears minimal. Hot reloading is highlighted as a key feature in the README, yet there's insufficient test coverage for file watching edge cases (race conditions, rapid file changes, symlinks). This would improve stability of the feature and prevent regressions.

  • [ ] Expand caddy/watcher_test.go with tests for concurrent file modifications and race conditions
  • [ ] Add tests in caddy/hotreload_test.go for symlink handling and nested directory watching
  • [ ] Test hot-reload behavior with large file change batches and debouncing logic
  • [ ] Add integration test validating worker processes restart gracefully without dropping connections

🌿Good first issues

  • Add missing test coverage for caddy/admin.go and caddy/admin_test.go: these files handle the Caddy admin API but lack integration tests; add tests for enabling/disabling FrankenPHP via the admin endpoint.
  • Document the Caddyfile configuration schema: no formal schema exists for the 'frankenphp' block syntax; create a JSON schema (caddy/schema.json) and example Caddyfiles for common patterns (Laravel, Symfony, static files).
  • Implement health check support in caddy/app.go: add a /health or /_internal/health endpoint that doesn't invoke PHP, useful for Kubernetes probes; this requires minimal changes to ServeHTTP routing.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 68573a9 — ci: run golangci-lint on the Caddy module (#2397) (dunglas)
  • 24be668 — docs: add SEO frontmatter, llms.txt, and code-block hygiene (#2394) (dunglas)
  • 2ac6c41 — feat: cross-platform force-kill primitive for stuck PHP threads (#2365) (nicolas-grekas)
  • 9208c55 — feat: persistent-zval helpers (deep-copy zval trees across threads) (#2366) (nicolas-grekas)
  • 2857012 — fix(extgen): emit parser warnings to stderr instead of stdout (#2374) (alexandre-daubois)
  • a1334b6 — refactor: add drain() seam to threadHandler interface (#2367) (nicolas-grekas)
  • 09bf047 — ci: reinstall libbrotli-dev on Linux test runners (#2391) (dunglas)
  • cc8dfa8 — docs: improve worker docs, and add internals docs (#2334) (dunglas)
  • a255768 — fix(extgen): better error handling (#2370) (dunglas)
  • bcc944a — chore: better preflight checks in release.sh (#2348) (dunglas)

🔒Security observations

  • High · Unverified Remote Script Execution in Dockerfile — Dockerfile, lines with curl and install-php-extensions. The Dockerfile downloads and executes install-php-extensions script from GitHub without checksum verification. This poses a significant supply chain risk if the GitHub release is compromised or during a MITM attack. Fix: Verify the downloaded script using SHA256 checksums or GPG signatures before execution. Store checksums in the repository and compare before execution: curl -sSLf <url> | sha256sum -c <expected_hash> && chmod +x
  • High · Go Module Retraction Without Clear Reason — caddy/go.mod, line with retract statement. The go.mod file contains 'retract v1.0.0-rc.1 // Human error' which is vague and doesn't document what the human error was or what versions are safe. This could mislead users about which versions to use. Fix: Document specific security issues or breaking changes that caused the retraction. Update to: 'retract v1.0.0-rc.1 // [Specific reason: CVE-XXXX or breaking change description]'
  • Medium · Missing HEALTHCHECK Timeout Configuration — Dockerfile, HEALTHCHECK line. The Dockerfile HEALTHCHECK command lacks timeout and retry parameters, which could lead to false negatives or slow detection of failures in container orchestration scenarios. Fix: Add timeout and retry parameters: HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl -f http://localhost:2019/me
  • Medium · Insecure Caddy Admin Endpoint Exposure — Dockerfile, HEALTHCHECK command and caddy/admin.go. The HEALTHCHECK uses port 2019 (Caddy admin API) without authentication verification. If the admin API is exposed internally or externally, it could allow unauthorized configuration changes. Fix: Ensure the Caddy admin API is only accessible from localhost or protected with authentication. Review caddy/admin.go for access control mechanisms and configure firewall rules accordingly.
  • Medium · Overly Permissive Dockerfile Checkov Skip Directives — Dockerfile, lines 1-3. The Dockerfile contains multiple unchecked 'checkov:skip' directives (CKV_DOCKER_2, 3, 7) which disable security checks without documented justification. This could hide security best practice violations. Fix: Document why each check is skipped (e.g., 'CKV_DOCKER_2: Non-root user not required for app server functionality'). Consider implementing the checks or using specific conditions rather than blanket skips.
  • Medium · No Image Signature Verification in Dockerfile — Dockerfile, FROM php-base line. The base image (php-base) is used without verifying its signature or digest, making it vulnerable to image tampering attacks. Fix: Use explicit image digests: FROM php-base@sha256:... and regularly update and verify digests from the official PHP Docker registry.
  • Low · Dependency Version Constraints Not Strict — caddy/go.mod and go.sum. While go.mod uses specific versions, some transitive dependencies with 'indirect' markers don't have pinned versions in lock file, potentially allowing minor updates that could introduce vulnerabilities. Fix: Ensure go.sum includes all indirect dependencies with exact versions. Run go mod tidy && go mod verify regularly and commit go.sum to version control.
  • Low · Missing COPY Validation in Dockerfile — Dockerfile, COPY --link caddy/frankenphp/Caddyfile line. The Dockerfile COPYs the Caddyfile without validating its syntax or content before use, potentially allowing misconfiguration. Fix: Add a validation step: RUN caddy validate --config /etc/caddy/Caddyfile after copying to fail fast on invalid configurations.
  • Low · Security Policy Mentions Email Address — undefined. SECURITY.md publicly lists a specific email address for security reports. While this is intentional Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · php/frankenphp — RepoPilot