diff --git a/.gitleaksignore b/.gitleaksignore index c36609fe58..831e30a21b 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -1,2 +1 @@ -/github/workspace/docs/mercure.md:jwt:88 -/github/workspace/docs/mercure.md:jwt:90 +/github/workspace/docs/mercure.md:jwt:95 diff --git a/.markdown-lint.yaml b/.markdown-lint.yaml index b37e37765f..17f04110fd 100644 --- a/.markdown-lint.yaml +++ b/.markdown-lint.yaml @@ -1,5 +1,7 @@ --- MD010: false MD013: false +MD025: + front_matter_title: "" MD033: false MD060: false diff --git a/docs/classic.md b/docs/classic.md index dd767a8fcc..a658d9148b 100644 --- a/docs/classic.md +++ b/docs/classic.md @@ -1,4 +1,9 @@ -# Using Classic Mode +--- +title: FrankenPHP classic mode: drop-in PHP-FPM replacement +description: Run FrankenPHP in classic mode as a drop-in replacement for PHP-FPM or Apache mod_php, with a fixed or autoscaling thread pool serving PHP files directly. +--- + +# Using classic mode Without any additional configuration, FrankenPHP operates in classic mode. In this mode, FrankenPHP functions like a traditional PHP server, directly serving PHP files. This makes it a seamless drop-in replacement for PHP-FPM or Apache with mod_php. diff --git a/docs/compile.md b/docs/compile.md index caae700126..065006a32b 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -1,6 +1,11 @@ -# Compile From Sources +--- +title: Compile FrankenPHP from sources with libphp.so +description: Build FrankenPHP from source on Linux, macOS and FreeBSD, linking PHP as a shared library via xcaddy or go build, and add custom Caddy modules and extensions. +--- -This document explains how to create a FrankenPHP binary that will load PHP as a dynamic library. +# Compile from sources + +This document explains how to create a FrankenPHP binary that will load PHP as a shared library. This is the recommended method. Alternatively, [fully and mostly static builds](static.md) can also be created. @@ -22,7 +27,7 @@ brew install shivammathur/php/php-zts brotli watcher brew link --overwrite --force shivammathur/php/php-zts ``` -### By Compiling PHP +### By compiling PHP Alternatively, you can compile PHP from sources with the options needed by FrankenPHP by following these steps. @@ -36,7 +41,7 @@ cd php-*/ Then, run the `configure` script with the options needed for your platform. The following `./configure` flags are mandatory, but you can add others, for example, to compile extensions or additional features. -#### Linux +#### Linux and FreeBSD ```console ./configure \ @@ -74,7 +79,7 @@ make -j"$(getconf _NPROCESSORS_ONLN)" sudo make install ``` -## Install Optional Dependencies +## Install optional dependencies Some FrankenPHP features depend on optional system dependencies that must be installed. Alternatively, these features can be disabled by passing build tags to the Go compiler. @@ -85,7 +90,7 @@ Alternatively, these features can be disabled by passing build tags to the Go co | Restart workers on file change | [Watcher C](https://github.com/e-dant/watcher/tree/release/watcher-c) | nowatcher | | [Mercure](mercure.md) | [Mercure Go library](https://pkg.go.dev/github.com/dunglas/mercure) (automatically installed, AGPL licensed) | nomercure | -## Compile the Go App +## Compile the Go app You can now build the final binary. diff --git a/docs/config.md b/docs/config.md index e16d1fa185..575d8e12e9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,3 +1,8 @@ +--- +title: Configuring FrankenPHP with Caddyfile, php.ini, and env vars +description: Configure FrankenPHP and Caddy via Caddyfile, JSON, or environment variables, including PHP runtime tuning, worker mode, file watching, and module options. +--- + # Configuration FrankenPHP, Caddy as well as the [Mercure](mercure.md) and [Vulcain](https://vulcain.rocks) modules can be configured using [the formats supported by Caddy](https://caddyserver.com/docs/getting-started#your-first-config). @@ -73,7 +78,7 @@ PHP: - PHP extensions: cannot be loaded, bundle them in the binary itself - copy one of `php.ini-production` or `php.ini-development` provided [in the PHP sources](https://github.com/php/php-src/). -## Caddyfile Config +## Caddyfile config The `php_server` or the `php` [HTTP directives](https://caddyserver.com/docs/caddyfile/concepts#directives) may be used within the site blocks to serve your PHP app. @@ -196,7 +201,7 @@ php_server [] { } ``` -### Watching for File Changes +### Watching for file changes Since workers only boot your application once and keep it in memory, any changes to your PHP files will not be reflected immediately. @@ -243,7 +248,7 @@ where the FrankenPHP process was started. You can instead also specify one or mo The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). -## Matching the Worker To a Path +## Matching the worker to a path In traditional PHP applications, scripts are always placed in the public directory. This is also true for worker scripts, which are treated like any other PHP script. @@ -266,7 +271,7 @@ and otherwise forward the request to the worker matching the path pattern. } ``` -## Restarting Threads After a Number of Requests (Experimental) +## Restarting threads after a number of requests (experimental) FrankenPHP can automatically restart PHP threads after they have handled a given number of requests. When a thread reaches the limit, it is fully restarted, @@ -285,7 +290,7 @@ But when the fix depends on a third party you don't control, } ``` -## Environment Variables +## Environment variables The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it: @@ -302,7 +307,7 @@ The `S` value of [the `variables_order` PHP directive](https://www.php.net/manua To load [additional PHP configuration files](https://www.php.net/manual/en/configuration.file.php#configuration.file.scan), the `PHP_INI_SCAN_DIR` environment variable can be used. -When set, PHP will load all the file with the `.ini` extension present in the given directories. +When set, PHP will load all the files with the `.ini` extension present in the given directories. You can also change the PHP configuration using the `php_ini` directive in the `Caddyfile`: @@ -323,14 +328,14 @@ You can also change the PHP configuration using the `php_ini` directive in the ` ### Disabling HTTPS -By default, FrankenPHP will automatically enable HTTPS using for all the hostnames, including `localhost`. +By default, FrankenPHP will automatically enable HTTPS for all the hostnames, including `localhost`. If you want to disable HTTPS (for example in a development environment), you can set the `SERVER_NAME` environment variable to `http://` or `:80`: Alternatively, you can use all other methods described in the [Caddy documentation](https://caddyserver.com/docs/automatic-https#activation). If you want to use HTTPS with the `127.0.0.1` IP address instead of the `localhost` hostname, please read the [known issues](known-issues.md#using-https127001-with-docker) section. -### Full Duplex (HTTP/1) +### Full duplex (HTTP/1) When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body has been read. (for example: [Mercure](mercure.md), WebSocket, Server-Sent Events, etc.) @@ -358,7 +363,7 @@ CADDY_GLOBAL_OPTIONS="servers { You can find more information about this setting in the [Caddy documentation](https://caddyserver.com/docs/caddyfile/options#enable-full-duplex). -## Enable the Debug Mode +## Enable the debug mode When using the Docker image, set the `CADDY_GLOBAL_OPTIONS` environment variable to `debug` to enable the debug mode: @@ -369,7 +374,7 @@ docker run -v $PWD:/app/public \ dunglas/frankenphp ``` -## Shell Completion +## Shell completion FrankenPHP provides built-in shell completion support for Bash, Zsh, Fish, and PowerShell. This enables autocompletion for all commands (including custom commands like `php-server`, `php-cli`, and `extension-init`) and their flags. @@ -441,5 +446,3 @@ Add-Content -Path $PROFILE -Value '. (Join-Path (Split-Path $PROFILE) "frankenph ``` You will need to start a new shell for this setup to take effect. - -You will need to start a new shell for this setup to take effect. diff --git a/docs/docker.md b/docs/docker.md index 3dcf65beb7..ed39639d10 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,4 +1,9 @@ -# Building Custom Docker Image +--- +title: FrankenPHP Docker image: build, configure, extend +description: Build custom FrankenPHP Docker images, install PHP extensions and Caddy modules, run as non-root, harden with distroless, and enable worker mode by default. +--- + +# Building a custom Docker image [FrankenPHP Docker images](https://hub.docker.com/r/dunglas/frankenphp) are based on [official PHP images](https://hub.docker.com/_/php/). Debian and Alpine Linux variants are provided for popular architectures. @@ -13,7 +18,7 @@ The tags follow this pattern: `dunglas/frankenphp:-php If you're using Alpine Linux and Symfony, > you may need to [increase the default stack size](compile.md#using-xcaddy). -## Enabling the Worker Mode by Default +## Enabling the worker mode by default Set the `FRANKENPHP_CONFIG` environment variable to start FrankenPHP with a worker script: @@ -106,7 +111,7 @@ FROM dunglas/frankenphp ENV FRANKENPHP_CONFIG="worker ./public/index.php" ``` -## Using a Volume in Development +## Using a volume in development To develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container: @@ -147,9 +152,9 @@ volumes: caddy_config: ``` -## Running as a Non-Root User +## Running as a non-root user -FrankenPHP can run as non-root user in Docker. +FrankenPHP can run as a non-root user in Docker. Here is a sample `Dockerfile` doing this: @@ -170,7 +175,7 @@ EOF USER ${USER} ``` -### Running With No Capabilities +### Running with no capabilities Even when running rootless, FrankenPHP needs the `CAP_NET_BIND_SERVICE` capability to bind the web server on privileged ports (80 and 443). @@ -198,14 +203,14 @@ USER ${USER} Next, set the `SERVER_NAME` environment variable to use an unprivileged port. Example: `:8000` -## Updates +## FrankenPHP Docker image updates The Docker images are built: - when a new release is tagged - daily at 4 am UTC, if new versions of the official PHP images are available -## Hardening Images +## Hardening images To further reduce the attack surface and size of your FrankenPHP Docker images, it's also possible to build them on top of a [Google distroless](https://github.com/GoogleContainerTools/distroless) or @@ -265,7 +270,7 @@ WORKDIR /app ENTRYPOINT ["/usr/local/bin/frankenphp", "run", "--config", "/etc/caddy/Caddyfile"] ``` -## Development Versions +## Development versions Development versions are available in the [`dunglas/frankenphp-dev`](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev) Docker repository. A new build is triggered every time a commit is pushed to the main branch of the GitHub repository. diff --git a/docs/early-hints.md b/docs/early-hints.md index ae8e656dc8..304d01d5b5 100644 --- a/docs/early-hints.md +++ b/docs/early-hints.md @@ -1,3 +1,8 @@ +--- +title: Sending HTTP 103 Early Hints from PHP with FrankenPHP +description: FrankenPHP natively supports the HTTP 103 Early Hints status code, letting PHP applications preload assets before the final response is ready. +--- + # Early Hints FrankenPHP natively supports the [103 Early Hints status code](https://developer.chrome.com/blog/early-hints/). diff --git a/docs/embed.md b/docs/embed.md index 5c6befa1e1..627a7e3ca4 100644 --- a/docs/embed.md +++ b/docs/embed.md @@ -1,4 +1,9 @@ -# PHP Apps As Standalone Binaries +--- +title: Embedding PHP apps as standalone binaries with FrankenPHP +description: How to package a PHP application (including Symfony or Laravel) as a self-contained static binary with FrankenPHP, the PHP interpreter, PHP extensions and Caddy. +--- + +# PHP apps as standalone binaries FrankenPHP has the ability to embed the source code and assets of PHP applications in a static, self-contained binary. @@ -8,7 +13,7 @@ Learn more about this feature [in the presentation made by Kévin at SymfonyCon For embedding Laravel applications, [read this specific documentation entry](laravel.md#laravel-apps-as-standalone-binaries). -## Preparing Your App +## Preparing your app Before creating the self-contained binary be sure that your app is ready for embedding. @@ -42,18 +47,19 @@ composer install --ignore-platform-reqs --no-dev -a composer dump-env prod ``` -### Customizing the Configuration +### Customizing the configuration To customize [the configuration](config.md), you can put a `Caddyfile` as well as a `php.ini` file in the main directory of the app to be embedded (`$TMPDIR/my-prepared-app` in the previous example). -## Creating a Linux Binary +## Creating a Linux binary The easiest way to create a Linux binary is to use the Docker-based builder we provide. 1. Create a file named `static-build.Dockerfile` in the repository of your app: ```dockerfile + # static-build.Dockerfile FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu # If you intend to run the binary on musl-libc systems, use static-builder-musl instead @@ -85,7 +91,7 @@ The easiest way to create a Linux binary is to use the Docker-based builder we p The resulting binary is the file named `my-app` in the current directory. -## Creating a Binary for Other OSes +## Creating a binary for other OSes If you don't want to use Docker, or want to build a macOS binary, use the shell script we provide: @@ -97,7 +103,7 @@ EMBED=/path/to/your/app ./build-static.sh The resulting binary is the file named `frankenphp--` in the `dist/` directory. -## Using The Binary +## Using the binary This is it! The `my-app` file (or `dist/frankenphp--` on other OSes) contains your self-contained app! @@ -125,18 +131,18 @@ You can also run the PHP CLI scripts embedded in your binary: ./my-app php-cli bin/console ``` -## PHP Extensions +## PHP extensions By default, the script will build extensions required by the `composer.json` file of your project, if any. If the `composer.json` file doesn't exist, the default extensions are built, as documented in [the static builds entry](static.md). To customize the extensions, use the `PHP_EXTENSIONS` environment variable. -## Customizing The Build +## Customizing the build [Read the static build documentation](static.md) to see how to customize the binary (extensions, PHP version...). -## Distributing The Binary +## Distributing the binary On Linux, the created binary is compressed using [UPX](https://upx.github.io). diff --git a/docs/extension-workers.md b/docs/extension-workers.md index dd8527d272..370be23a41 100644 --- a/docs/extension-workers.md +++ b/docs/extension-workers.md @@ -1,14 +1,20 @@ -# Extension Workers +--- +title: FrankenPHP extension workers: background PHP thread pools +description: Use FrankenPHP Extension Workers to run a dedicated PHP thread pool from a Go extension for queues, schedulers, event listeners, and custom protocols. +--- + +# Extension workers Extension Workers enable your [FrankenPHP extension](https://frankenphp.dev/docs/extensions/) to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Useful for queue systems, event listeners, schedulers, etc. -## Registering the Worker +## Registering the worker -### Static Registration +### Static registration If you don't need to make the worker configurable by the user (fixed script path, fixed number of threads), you can simply register the worker in the `init()` function. ```go +// FrankenPHP extension worker static registration package myextension import ( @@ -33,25 +39,26 @@ func init() { } ``` -### In a Caddy Module (Configurable by the user) +### In a Caddy module (configurable by the user) -If you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their `Caddyfile`. This requires implementing the `caddy.Provisioner` interface and parsing the Caddyfile ([see an example](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)). +If you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their `Caddyfile`. This requires implementing the `caddy.Provisioner` interface and parsing the Caddyfile ([see the frankenphp-queue Caddy module example](https://github.com/dunglas/frankenphp-queue/blob/main/caddy.go)). -### In a Pure Go Application (Embedding) +### In a pure Go application (embedding) If you are [embedding FrankenPHP in a standard Go application without caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), you can register extension workers using `frankenphp.WithExtensionWorkers` when initializing options. -## Interacting with Workers +## Interacting with workers -Once the worker pool is active, you can dispatch tasks to it. This can be done inside [native functions exported to PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a any other goroutine. +Once the worker pool is active, you can dispatch tasks to it. This can be done inside [native functions exported to PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or any other goroutine. -### Headless Mode : `SendMessage` +### Headless mode: `SendMessage` Use `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands. -#### Example: An Async Queue Extension +#### Async queue extension example ```go +// FrankenPHP extension: dispatch raw messages to a worker via SendMessage // #include import "C" import ( @@ -78,11 +85,12 @@ func my_queue_push(data *C.zval) bool { } ``` -### HTTP Emulation :`SendRequest` +### HTTP emulation: `SendRequest` Use `SendRequest` if your extension needs to invoke a PHP script that expects a standard web environment (populating `$_SERVER`, `$_GET`, etc.). ```go +// FrankenPHP extension: invoke a worker PHP script via SendRequest (HTTP emulation) // #include import "C" import ( @@ -109,13 +117,13 @@ func my_worker_http_request(path *C.zend_string) unsafe.Pointer { } ``` -## Worker Script +## Worker script The PHP worker script runs in a loop and can handle both raw messages and HTTP requests. ```php @@ -80,9 +86,9 @@ There are two important things to note here: - A directive comment `//export_php:function` defines the function signature in PHP. This is how the generator knows how to generate the PHP function with the right parameters and return type. - The function must return an `unsafe.Pointer`. FrankenPHP provides an API to help you with type juggling between C and Go. -While the first point speaks for itself, the second may be harder to apprehend. Let's take a deeper dive to type juggling later in this guide. +While the first point speaks for itself, the second may be harder to apprehend. Let's take a deeper dive into type juggling later in this guide. -### Generating the Extension +### Generating the extension This is where the magic happens, and your extension can now be generated. You can run the generator with the following command: @@ -106,7 +112,7 @@ If everything went well, your project directory should contain the following fil > [!IMPORTANT] > **Your source file (`my_extension.go`) is never modified.** The generator creates a separate `_generated.go` file containing CGO wrappers that call your original functions. This means you can safely version control your source file without worrying about generated code polluting it. -### Integrating the Generated Extension into FrankenPHP +### Integrating the generated extension into FrankenPHP Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module: @@ -122,7 +128,7 @@ xcaddy build \ Note that you point to the `/build` subdirectory that was created during the generation step. However, this is not mandatory: you can also copy the generated files to your module directory and point to it directly. -### Testing Your Generated Extension +### Testing your generated extension You can create a PHP file to test the functions and classes you've created. For example, create an `index.php` file with the following content: @@ -140,7 +146,7 @@ echo $processor->process('Hello World', StringProcessor::MODE_UPPERCASE); // "H Once you've integrated your extension into FrankenPHP as demonstrated in the previous section, you can run this test file using `./frankenphp php-server`, and you should see your extension working. -### Type Juggling +### Type juggling While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding the internals of the Zend Engine and how variables are stored internally in PHP. This table summarizes what you need to know: @@ -180,6 +186,7 @@ If order or association are not needed, it's also possible to directly convert t **Creating and manipulating arrays in Go:** ```go +// Converting between PHP arrays and Go maps/slices package example // #include @@ -272,13 +279,14 @@ func process_data_packed(arr *C.zend_array) unsafe.Pointer { - `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice - `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs) -### Working with Callables +### Working with callables FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code. To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results: ```go +// Calling a PHP callable from a Go-defined extension function // export_php:function my_array_map(array $data, callable $callback): array func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer { goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr)) @@ -308,11 +316,12 @@ $result = my_array_map(['hello', 'world'], 'strtoupper'); // $result will be ['HELLO', 'WORLD'] ``` -### Declaring a Native PHP Class +### Declaring a native PHP class The generator supports declaring **opaque classes** as Go structs, which can be used to create PHP objects. You can use the `//export_php:class` directive comment to define a PHP class. For example: ```go +// Declaring a PHP class backed by a Go struct package example //export_php:class User @@ -322,7 +331,7 @@ type UserStruct struct { } ``` -#### What are Opaque Classes? +#### What are opaque classes? **Opaque classes** are classes where the internal structure (properties) is hidden from PHP code. This means: @@ -330,15 +339,16 @@ type UserStruct struct { - **Method-only interface** - All interactions must go through methods you define - **Better encapsulation** - Internal data structure is completely controlled by Go code - **Type safety** - No risk of PHP code corrupting internal state with wrong types -- **Cleaner API** - Forces to design a proper public interface +- **Cleaner API** - Forces you to design a proper public interface This approach provides better encapsulation and prevents PHP code from accidentally corrupting the internal state of your Go objects. All interactions with the object must go through the methods you explicitly define. -#### Adding Methods to Classes +#### Adding methods to classes Since properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior: ```go +// Defining methods on a Go-backed PHP class package example // #include @@ -376,11 +386,12 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) { } ``` -#### Nullable Parameters +#### Nullable parameters The generator supports nullable parameters using the `?` prefix in PHP signatures. When a parameter is nullable, it becomes a pointer in your Go function, allowing you to check if the value was `null` in PHP: ```go +// Handling nullable PHP parameters in a Go method package example // #include @@ -446,15 +457,16 @@ $user->updateInfo(null, 25, null); // Name and active are null This design ensures that your Go code has complete control over how the object's state is accessed and modified, providing better encapsulation and type safety. -### Declaring Constants +### Declaring constants The generator supports exporting Go constants to PHP using two directives: `//export_php:const` for global constants and `//export_php:classconst` for class constants. This allows you to share configuration values, status codes, and other constants between Go and PHP code. -#### Global Constants +#### Global constants Use the `//export_php:const` directive to create global PHP constants: ```go +// Exporting global PHP constants from Go package example //export_php:const @@ -474,11 +486,12 @@ const ( > > PHP constants will take the name of the Go constant, thus using upper case letters is recommended. -#### Class Constants +#### Class constants Use the `//export_php:classconst ClassName` directive to create constants that belong to a specific PHP class: ```go +// Exporting PHP class constants from Go package example //export_php:classconst User @@ -522,6 +535,7 @@ The directive supports various value types, including strings, integers, boolean You can use constants just like you are used to in the Go code. For example, let's take the `repeat_this()` function we declared earlier and change the last argument to an integer: ```go +// Combining functions, classes, methods, and constants in one extension package example // #include @@ -581,7 +595,7 @@ func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsaf } ``` -### Using Namespaces +### Using namespaces The generator supports organizing your PHP extension's functions, classes, and constants under a namespace using the `//export_php:namespace` directive. This helps avoid naming conflicts and provides better organization for your extension's API. @@ -590,6 +604,7 @@ The generator supports organizing your PHP extension's functions, classes, and c Use the `//export_php:namespace` directive at the top of your Go file to place all exported symbols under a specific namespace: ```go +// Placing exported symbols under a PHP namespace //export_php:namespace My\Extension package example @@ -618,7 +633,7 @@ func (u *UserStruct) GetName() unsafe.Pointer { const STATUS_ACTIVE = 1 ``` -#### Using Namespaced Extension in PHP +#### Using namespaced extension in PHP When a namespace is declared, all functions, classes, and constants are placed under that namespace in PHP: @@ -633,26 +648,27 @@ echo $user->getName(); // "John Doe" echo My\Extension\STATUS_ACTIVE; // 1 ``` -#### Important Notes +#### Important notes - Only **one** namespace directive is allowed per file. If multiple namespace directives are found, the generator will return an error. - The namespace applies to **all** exported symbols in the file: functions, classes, methods, and constants. - Namespace names follow PHP namespace conventions using backslashes (`\`) as separators. - If no namespace is declared, symbols are exported to the global namespace as usual. -## Manual Implementation +## Manual implementation If you want to understand how extensions work or need full control over your extension, you can write them manually. This approach gives you complete control but requires more boilerplate code. -### Basic Function +### Basic function We'll see how to write a simple PHP extension in Go that defines a new native function. This function will be called from PHP and will trigger a goroutine that logs a message in Caddy's logs. This function doesn't take any parameters and returns nothing. -#### Define the Go Function +#### Define the Go function In your module, you need to define a new native function that will be called from PHP. To do this, create a file with the name you want, for example, `extension.go`, and add the following code: ```go +// extension.go package example // #include "extension.h" @@ -686,6 +702,7 @@ To allow PHP to call our function, we need to define a corresponding PHP functio ```php #include "extension.h" #include "extension_arginfo.h" @@ -764,16 +783,17 @@ Finally, we define the extension's metadata in a `zend_module_entry` structure, The extension registration is automatically handled by FrankenPHP's `RegisterExtension()` function that we call in our Go code. -### Advanced Usage +### Advanced usage Now that we know how to create a basic PHP extension in Go, let's complexify our example. We will now create a PHP function that takes a string as a parameter and returns its uppercase version. -#### Define the PHP Function Stub +#### Define the PHP function stub To define the new PHP function, we will modify our `extension.stub.php` file to include the new function signature: ```php @@ -877,7 +900,7 @@ The `false` parameter in `PHPString()` indicates that we want to create a new no > > In this example, we don't perform any error handling, but you should always check that pointers are not `nil` and that the data is valid before using it in your Go functions. -### Integrating the Extension into FrankenPHP +### Integrating the extension into FrankenPHP Our extension is now ready to be compiled and integrated into FrankenPHP. To do this, refer to the FrankenPHP [compilation documentation](compile.md) to learn how to compile FrankenPHP. Add the module using the `--with` flag, pointing to the path of your module: @@ -893,7 +916,7 @@ xcaddy build \ That's it! Your extension is now integrated into FrankenPHP and can be used in your PHP code. -### Testing Your Extension +### Testing your extension After integrating your extension into FrankenPHP, you can create an `index.php` file with examples for the functions you've implemented: diff --git a/docs/github-actions.md b/docs/github-actions.md index f9f9afab80..e7663a440f 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -1,3 +1,8 @@ +--- +title: Building FrankenPHP Docker images with GitHub Actions +description: Use GitHub Actions to automatically build and publish FrankenPHP Docker images to a registry on pull requests, merges to main, and tagged releases. +--- + # Using GitHub Actions This repository builds and deploys the Docker image to [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) on @@ -12,13 +17,13 @@ In the repository settings, under secrets, add the following secrets: - `REGISTRY_PASSWORD`: The password to use to log in to the registry (e.g. an access key). - `IMAGE_NAME`: The name of the image (e.g. `dunglas/frankenphp`). -## Building and Pushing the Image +## Building and pushing the image 1. Create a Pull Request or push to your fork. 2. GitHub Actions will build the image and run any tests. 3. If the build is successful, the image will be pushed to the registry using the `pr-x`, where `x` is the PR number, as the tag. -## Deploying the Image +## Deploying the image 1. Once the Pull Request is merged, GitHub Actions will again run the tests and build a new image. 2. If the build is successful, the `main` tag will be updated in the Docker registry. diff --git a/docs/hot-reload.md b/docs/hot-reload.md index 49790d1e68..967fdcc54d 100644 --- a/docs/hot-reload.md +++ b/docs/hot-reload.md @@ -1,4 +1,9 @@ -# Hot Reload +--- +title: FrankenPHP hot reload for PHP, templates, and assets +description: Use FrankenPHP hot reload to update PHP code, templates, and frontend assets in the browser without manual refresh, with optional DOM morphing via Idiomorph. +--- + +# Hot reload FrankenPHP includes a built-in **hot reload** feature designed to vastly improve the developer experience. @@ -18,7 +23,7 @@ Depending on your setup, the browser will either: - **Morph the DOM** (preserving scroll position and input state) if [Idiomorph](https://github.com/bigskysoftware/idiomorph) is loaded. - **Reload the page** (standard live reload) if Idiomorph is not present. -## Configuration +## Enabling FrankenPHP hot reload To enable hot reloading, enable Mercure, then add the `hot_reload` sub-directive to the `php_server` directive in your `Caddyfile`. @@ -78,7 +83,7 @@ php_server { } ``` -## Client-Side Integration +## Client-side integration for FrankenPHP hot reload While the server detects changes, the browser needs to subscribe to these events to update the page. FrankenPHP exposes the Mercure Hub URL to use for subscribing to file changes via the `$_SERVER['FRANKENPHP_HOT_RELOAD']` environment variable. @@ -101,7 +106,7 @@ It is available as an [npm](https://www.npmjs.com/package/frankenphp-hot-reload) Alternatively, you can implement your own client-side logic by subscribing directly to the Mercure hub using the `EventSource` native JavaScript class. -### Preserving Existing DOM Nodes +### Preserving existing DOM nodes In rare cases, such as when using development tools [like the Symfony web debug toolbar](https://github.com/symfony/symfony/pull/62970), you may want to preserve specific DOM nodes. @@ -111,7 +116,7 @@ To do so, add the `data-frankenphp-hot-reload-preserve` attribute to the relevan
``` -## Worker Mode +## Hot reload with FrankenPHP worker mode If you are running your application in [Worker Mode](https://frankenphp.dev/docs/worker/), your application script remains in memory. This means changes to your PHP code will not be reflected immediately, even if the browser reloads. @@ -121,7 +126,8 @@ For the best developer experience, you should combine `hot_reload` with [the `wa - `hot_reload`: refreshes the **browser** when files change - `worker.watch`: restarts the worker when files change -```caddy +```caddyfile +# Caddyfile: combine FrankenPHP hot reload with worker watch for full dev workflow localhost mercure { @@ -138,7 +144,7 @@ php_server { } ``` -## How It Works +## How FrankenPHP hot reload works 1. **Watch**: FrankenPHP monitors the filesystem for modifications using [the `e-dant/watcher` library](https://github.com/e-dant/watcher) under the hood (we contributed the Go binding). 2. **Restart (Worker Mode)**: if `watch` is enabled in the worker config, the PHP worker is restarted to load the new code. diff --git a/docs/internals.md b/docs/internals.md index bd1bb636fa..97091b8cac 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -1,8 +1,13 @@ +--- +title: FrankenPHP internals: threads, state machine, and CGO +description: How FrankenPHP works inside: PHP ZTS thread pool, Go state machine, CGO boundary with the PHP SAPI, auto-scaling, and per-thread environment sandboxing. +--- + # Internals This document explains FrankenPHP's internal architecture, focusing on thread management, the state machine, and the CGO boundary between Go and C/PHP. -## Overview +## FrankenPHP architecture overview FrankenPHP embeds the PHP interpreter directly into Go via CGO. Each PHP execution runs on a real POSIX thread (not a goroutine) because PHP's ZTS (Zend Thread Safety) model requires it. Go orchestrates these threads through a state machine, while C handles the PHP SAPI lifecycle. @@ -12,9 +17,9 @@ The main layers are: 2. **C layer** (`frankenphp.c`, `frankenphp.h`): PHP SAPI implementation, script execution loop, superglobal management 3. **State machine** (`internal/state/`): Synchronization between Go goroutines and C threads -## Thread Types +## FrankenPHP thread types -### Main Thread (`phpmainthread.go`) +### Main thread (`phpmainthread.go`) The main PHP thread (`phpMainThread`) initializes the PHP runtime: @@ -25,7 +30,7 @@ The main PHP thread (`phpMainThread`) initializes the PHP runtime: It stays alive for the lifetime of the server. All other threads are started after it signals `Ready`. -### Regular Threads (`threadregular.go`) +### Regular threads (`threadregular.go`) Handle classic one-request-per-invocation PHP scripts. Each request: @@ -34,7 +39,7 @@ Handle classic one-request-per-invocation PHP scripts. Each request: 3. The C layer executes the PHP script 4. `afterScriptExecution()` closes the request context -### Worker Threads (`threadworker.go`) +### Worker threads (`threadworker.go`) Keep a PHP script alive across multiple requests. The PHP script calls `frankenphp_handle_request()` in a loop: @@ -48,11 +53,11 @@ Keep a PHP script alive across multiple requests. The PHP script calls `frankenp After the script exits, the worker is restarted immediately if it had reached `frankenphp_handle_request()` at least once (whether the exit was clean or the result of a fatal error). Exponential backoff is only applied to consecutive startup failures, where the script exits before ever reaching `frankenphp_handle_request()`. -## Thread State Machine +## FrankenPHP thread state machine Each thread has a `ThreadState` (defined in `internal/state/state.go`) that governs its lifecycle. The state machine uses a `sync.RWMutex` for all state transitions and a channel-based subscriber pattern for blocking waits. -### States +### FrankenPHP thread states ```text Lifecycle: Reserved → BootRequested → Booting → Inactive → Ready ⇄ (processing) @@ -85,7 +90,7 @@ The full set of states is defined in `internal/state/state.go`: | `TransitionInProgress` | The C thread has acknowledged the transition request. | | `TransitionComplete` | The Go side has installed the new handler. | -### Key Operations +### Key state machine operations **`RequestSafeStateChange(nextState)`**: The primary way external goroutines request state changes. It: @@ -101,7 +106,7 @@ This guarantees mutual exclusion: only one of `shutdown()`, `setHandler()`, or ` **`CompareAndSwap(compareTo, swapTo)`**: Atomic compare-and-swap. Used for boot initialization. -### Handler Transition Protocol +### Handler transition protocol When a thread needs to change its handler (e.g., from inactive to worker): @@ -124,7 +129,7 @@ Set(TransitionComplete) This protocol ensures the handler pointer is never read and written concurrently. -### Worker Restart Protocol +### Worker restart protocol When workers are restarted (e.g., via admin API): @@ -148,9 +153,9 @@ Set(Ready) state is Ready → normal execution ``` -## CGO Boundary +## CGO boundary between Go and PHP -### Exported Go Functions +### Exported Go functions C code calls Go functions via CGO exports. The main callbacks are: @@ -169,11 +174,12 @@ C code calls Go functions via CGO exports. The main callbacks are: All these functions receive a `threadIndex` parameter identifying the calling thread. This is a thread-local variable in C (`__thread uintptr_t thread_index`) set during thread initialization. -### C Thread Main Loop +### C thread main loop Each PHP thread runs `php_thread()` in `frankenphp.c`: ```c +// frankenphp.c: php_thread() main script execution loop while ((scriptName = go_frankenphp_before_script_execution(thread_index))) { php_request_startup(); php_execute_script(&file_handle); @@ -184,17 +190,17 @@ while ((scriptName = go_frankenphp_before_script_execution(thread_index))) { Bailouts (fatal PHP errors) are caught by `zend_catch`, which marks the thread as unhealthy and forces cleanup. -### Memory Management +### Memory management across the CGO boundary - **Go → C strings**: `C.CString()` allocates with `malloc()`. The C side is responsible for freeing (e.g., `frankenphp_free_request_context()` frees cookie data). - **Go string pinning**: `phpThread` (in `phpthread.go`) embeds Go's [`runtime.Pinner`](https://pkg.go.dev/runtime#Pinner). `thread.Pin()` / `thread.Unpin()` keep Go memory referenced from C alive without copying it. The thread is unpinned after each script execution. - **PHP memory**: Managed by Zend's memory manager (`emalloc`/`efree`). Automatically freed at request shutdown. -## Auto-Scaling +## FrankenPHP thread auto-scaling FrankenPHP can automatically scale the number of PHP threads based on demand (`scaling.go`). -### Configuration +### Auto-scaling configuration - `num_threads`: Initial number of threads started at boot - `max_threads`: Maximum number of threads allowed (includes auto-scaled) @@ -215,7 +221,7 @@ A dedicated goroutine reads from an unbuffered `scaleChan`: A separate goroutine periodically checks (every 5s) for idle auto-scaled threads. Threads in `Ready` state idle longer than `maxIdleTime` (default 5s) are converted to `Inactive` (up to 10 per cycle). They are not fully stopped: a code path exists for that, but it is currently disabled because some PECL extensions leak memory and prevent threads from cleanly shutting down. -## Environment Sandboxing +## Per-thread environment sandboxing FrankenPHP sandboxes environment variables per-thread: @@ -225,7 +231,7 @@ FrankenPHP sandboxes environment variables per-thread: 4. `frankenphp_putenv()` / `frankenphp_getenv()` operate on a thread-local `sandboxed_env` initialized lazily from `main_thread_env`, preventing race conditions on the global C environment. 5. `reset_sandboxed_environment()` releases `sandboxed_env` after each PHP script execution. In regular mode that's per request; in worker mode it only runs when the worker script itself exits, so `putenv()` writes are visible to subsequent worker requests on the same thread until the script restarts. -## Request Flow (Regular Mode) +## Request flow (regular mode) 1. HTTP request arrives at Caddy 2. FrankenPHP's Caddy module resolves the PHP script path @@ -237,7 +243,7 @@ FrankenPHP sandboxes environment variables per-thread: 8. After execution, `afterScriptExecution()` signals completion 9. The response is sent to the client -## Request Flow (Worker Mode) +## Request flow (worker mode) 1. HTTP request arrives at Caddy 2. FrankenPHP's Caddy module resolves the worker for this request diff --git a/docs/known-issues.md b/docs/known-issues.md index da22690373..4dcc1a6498 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -1,6 +1,11 @@ -# Known Issues +--- +title: FrankenPHP known issues, incompatible extensions, and workarounds +description: Reference of FrankenPHP limitations, including unsupported PHP extensions, musl libc caveats, Docker TLS setup with 127.0.0.1, and Composer @php scripts. +--- -## Unsupported PHP Extensions +# Known issues + +## Unsupported PHP extensions The following extensions are known not to be compatible with FrankenPHP: @@ -9,7 +14,7 @@ The following extensions are known not to be compatible with FrankenPHP: | [imap](https://www.php.net/manual/en/imap.installation.php) | Not thread-safe | [javanile/php-imap2](https://github.com/javanile/php-imap2), [webklex/php-imap](https://github.com/Webklex/php-imap) | | [newrelic](https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/introduction-new-relic-php/) | Not thread-safe | - | -## Buggy PHP Extensions +## Buggy PHP extensions The following extensions have known bugs and unexpected behaviors when used with FrankenPHP: @@ -21,9 +26,9 @@ The following extensions have known bugs and unexpected behaviors when used with The [get_browser()](https://www.php.net/manual/en/function.get-browser.php) function seems to perform badly after a while. A workaround is to cache (e.g. with [APCu](https://www.php.net/manual/en/book.apcu.php)) the results per User Agent, as they are static. -## Standalone Binary and Alpine-based Docker Images +## Standalone binary and Alpine-based Docker images -The fully binary and Alpine-based Docker images (`dunglas/frankenphp:*-alpine`) use [musl libc](https://musl.libc.org/) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), to keep a smaller binary size. +The fully static binary and Alpine-based Docker images (`dunglas/frankenphp:*-alpine`) use [musl libc](https://musl.libc.org/) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), to keep a smaller binary size. This may lead to some compatibility issues. In particular, the glob flag `GLOB_BRACE` is [not available](https://www.php.net/manual/en/function.glob.php) @@ -80,7 +85,7 @@ docker run \ dunglas/frankenphp ``` -## Composer Scripts Referencing `@php` +## Composer scripts referencing `@php` [Composer scripts](https://getcomposer.org/doc/articles/scripts.md) may want to execute a PHP binary for some tasks, e.g. in [a Laravel project](laravel.md) to run `@php artisan package:discover --ansi`. This [currently fails](https://github.com/php/frankenphp/issues/483#issuecomment-1899890915) for two reasons: @@ -91,6 +96,7 @@ As a workaround, we can create a shell script in `/usr/local/bin/php` which stri ```bash #!/usr/bin/env bash +# /usr/local/bin/php args=("$@") index=0 for i in "$@" @@ -112,7 +118,7 @@ export PHP_BINARY=/usr/local/bin/php composer install ``` -## Troubleshooting TLS/SSL Issues with Static Binaries +## Troubleshooting TLS/SSL issues with static binaries When using the static binaries, you may encounter the following TLS-related errors, for instance when sending emails using STARTTLS: diff --git a/docs/laravel.md b/docs/laravel.md index 182557c9db..de9165ca09 100644 --- a/docs/laravel.md +++ b/docs/laravel.md @@ -1,6 +1,11 @@ +--- +title: Running Laravel with FrankenPHP (Docker, Octane, standalone binary) +description: How to run a Laravel application with FrankenPHP using the Docker image, a local install, Laravel Octane, or as an embedded standalone binary. +--- + # Laravel -## Docker +## Running Laravel with the FrankenPHP Docker image Serving a [Laravel](https://laravel.com) web application with FrankenPHP is as easy as mounting the project in the `/app` directory of the official Docker image. @@ -12,7 +17,7 @@ docker run -p 80:80 -p 443:443 -p 443:443/udp -v $PWD:/app dunglas/frankenphp And enjoy! -## Local Installation +## Installing Laravel with FrankenPHP locally Alternatively, you can run your Laravel projects with FrankenPHP from your local machine: @@ -20,6 +25,7 @@ Alternatively, you can run your Laravel projects with FrankenPHP from your local 2. Add the following configuration to a file named `Caddyfile` in the root directory of your Laravel project: ```caddyfile + # Caddyfile { frankenphp } @@ -74,13 +80,13 @@ The `octane:frankenphp` command can take the following options: - `--log-level`: Log messages at or above the specified log level, using the native Caddy logger > [!TIP] -> To get structured JSON logs (useful when using log analytics solutions), explicitly the pass `--log-level` option. +> To get structured JSON logs (useful when using log analytics solutions), explicitly pass the `--log-level` option. See also [how to use Mercure with Octane](#mercure-support). Learn more about [Laravel Octane in its official documentation](https://laravel.com/docs/octane). -## Laravel Apps As Standalone Binaries +## Laravel apps as standalone binaries Using [FrankenPHP's application embedding feature](embed.md), it's possible to distribute Laravel apps as standalone binaries. @@ -90,6 +96,7 @@ Follow these steps to package your Laravel app as a standalone binary for Linux: 1. Create a file named `static-build.Dockerfile` in the repository of your app: ```dockerfile + # static-build.Dockerfile FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu # If you intend to run the binary on musl-libc systems, use static-builder-musl instead @@ -162,23 +169,24 @@ Your app is now ready! Learn more about the options available and how to build binaries for other OSes in the [applications embedding](embed.md) documentation. -### Changing The Storage Path +### Changing the storage path By default, Laravel stores uploaded files, caches, logs, etc. in the application's `storage/` directory. This is not suitable for embedded applications, as each new version will be extracted into a different temporary directory. Set the `LARAVEL_STORAGE_PATH` environment variable (for example, in your `.env` file) or call the `Illuminate\Foundation\Application::useStoragePath()` method to use a directory outside the temporary directory. -### Mercure Support +### Mercure support [Mercure](https://mercure.rocks) is a great way to add real-time capabilities to your Laravel apps. FrankenPHP includes [Mercure support out of the box](mercure.md). If you are not using [Octane](#laravel-octane), see [the Mercure documentation entry](mercure.md). -If you are using Octane, you can use enable Mercure support by adding the following lines to your `config/octane.php` file: +If you are using Octane, you can enable Mercure support by adding the following lines to your `config/octane.php` file: ```php +// config/octane.php // ... return [ @@ -197,11 +205,11 @@ You can use [all directives supported by Mercure](https://mercure.rocks/docs/hub To publish and subscribe to updates, we recommend using the [Laravel Mercure Broadcaster](https://github.com/mvanduijker/laravel-mercure-broadcaster) library. Alternatively, see [the Mercure documentation](mercure.md) to do it in pure PHP and JavaScript. -### Running Octane With Standalone Binaries +### Running Octane with standalone binaries It's even possible to package Laravel Octane apps as standalone binaries! -To do so, [install Octane properly](#laravel-octane) and follow the steps described in [the previous section](#laravel-apps-as-standalone-binaries). +To do so, [install Octane properly](#laravel-octane) and follow the steps described in [the section on Laravel apps as standalone binaries](#laravel-apps-as-standalone-binaries). Then, to start FrankenPHP in worker mode through Octane, run: diff --git a/docs/logging.md b/docs/logging.md index 7f68b3dc00..e8df467aa6 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -1,3 +1,8 @@ +--- +title: FrankenPHP logging with frankenphp_log() and Caddy +description: Emit structured logs from PHP with frankenphp_log() or error_log() in FrankenPHP, routed through Caddy's logging system as JSON for Datadog, Loki, or Elastic. +--- + # Logging > [!TIP] diff --git a/docs/mercure.md b/docs/mercure.md index 25a619b878..ebf6541e50 100644 --- a/docs/mercure.md +++ b/docs/mercure.md @@ -1,3 +1,8 @@ +--- +title: Real-time updates with the FrankenPHP Mercure hub +description: FrankenPHP ships with a built-in Mercure hub for pushing real-time events to browsers over HTTP, as a simpler alternative to WebSockets. +--- + # Real-time FrankenPHP comes with a built-in [Mercure](https://mercure.rocks) hub! @@ -37,7 +42,7 @@ php_server > > Uncomment the Mercure section in `/etc/frankenphp/Caddyfile` to enable it. -## Subscribing to Updates +## Subscribing to updates By default, the Mercure hub is available on the `/.well-known/mercure` path of your FrankenPHP server. To subscribe to updates, use the native [`EventSource`](https://developer.mozilla.org/docs/Web/API/EventSource) JavaScript class: @@ -54,7 +59,7 @@ To subscribe to updates, use the native [`EventSource`](https://developer.mozill ``` -## Publishing Updates +## Publishing updates ### Using `mercure_publish()` @@ -102,19 +107,19 @@ $updateID = file_get_contents('https://localhost/.well-known/mercure', context: error_log("update $updateID published", 4); ``` -The key passed as parameter of the `mercure.publisher_jwt` option in the `Caddyfile` must used to sign the JWT token used in the `Authorization` header. +The key passed as parameter of the `mercure.publisher_jwt` option in the `Caddyfile` must be used to sign the JWT token used in the `Authorization` header. The JWT must include a `mercure` claim with a `publish` permission for the topics you want to publish to. See [the Mercure documentation](https://mercure.rocks/spec#publishers) about authorization. To generate your own tokens, you can use [this jwt.io link](https://www.jwt.io/#token=eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.PXwpfIGng6KObfZlcOXvcnWCJOWTFLtswGI5DZuWSK4), -but for production apps, it's recommended to use short-lived tokens generated aerodynamically using with a trusted [JWT library](https://www.jwt.io/libraries?programming_language=php). +but for production apps, it's recommended to use short-lived tokens generated dynamically using a trusted [JWT library](https://www.jwt.io/libraries?programming_language=php). ### Using Symfony Mercure Alternatively, you can use the [Symfony Mercure Component](https://symfony.com/components/Mercure), a standalone PHP library. -This library handled the JWT generation, update publishing as well as cookie-based authorization for subscribers. +This library handles the JWT generation, update publishing as well as cookie-based authorization for subscribers. First, install the library using Composer: diff --git a/docs/metrics.md b/docs/metrics.md index 83bae898ba..a783a5c90e 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -1,3 +1,8 @@ +--- +title: FrankenPHP Prometheus metrics for threads and workers +description: List of Prometheus-compatible metrics exposed by FrankenPHP via Caddy, covering PHP threads, workers, request times, queue depth, crashes, and restarts. +--- + # Metrics > [!TIP] @@ -17,4 +22,4 @@ When [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenP - `frankenphp_worker_restarts{worker="[worker_name]"}`: The number of times a worker has been deliberately restarted. - `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests. -For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise absolute path of worker file will be used. +For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise the absolute path of the worker file will be used. diff --git a/docs/migrate.md b/docs/migrate.md index 07016fef10..29a44f5688 100644 --- a/docs/migrate.md +++ b/docs/migrate.md @@ -1,9 +1,14 @@ +--- +title: Migrating from Nginx and PHP-FPM to FrankenPHP +description: Step-by-step guide to migrate a PHP application from an Nginx plus PHP-FPM stack to FrankenPHP, covering Caddyfile, php.ini, threads, and Docker. +--- + # Migrating from Nginx/PHP-FPM FrankenPHP replaces both your web server (Nginx, Apache) and PHP-FPM with a single binary. This guide covers a basic migration for a typical PHP application. -## Key Differences +## Key differences | PHP-FPM setup | FrankenPHP equivalent | | --------------------------------- | -------------------------------------------------------- | @@ -14,11 +19,12 @@ This guide covers a basic migration for a typical PHP application. | `pm = static` / `pm.max_children` | `num_threads` | | `pm = dynamic` | [`max_threads auto`](performance.md#max_threads) | -## Step 1: Replace Your Web Server Config +## Step 1: replace your web server config A typical Nginx + PHP-FPM configuration: ```nginx +# /etc/nginx/sites-available/example.com server { listen 80; server_name example.com; @@ -48,7 +54,7 @@ example.com { That's it. The `php_server` directive handles PHP routing, `try_files`-like behavior, and static file serving. -## Step 2: Migrate PHP Configuration +## Step 2: migrate PHP configuration Your existing `php.ini` works as-is. See [Configuration](config.md) for where to place it depending on your installation method. @@ -68,7 +74,7 @@ example.com { } ``` -## Step 3: Adjust Pool Size +## Step 3: adjust pool size In PHP-FPM, you tune `pm.max_children` to control the number of worker processes. In FrankenPHP, the equivalent is `num_threads`: @@ -92,13 +98,14 @@ By default, FrankenPHP starts 2 threads per CPU. For dynamic scaling similar to } ``` -## Step 4: Docker Migration +## Step 4: Docker migration A typical PHP-FPM Docker setup using two containers (Nginx + PHP-FPM) can be replaced by a single container: **Before:** ```yaml +# compose.yaml services: nginx: image: nginx:1 @@ -119,6 +126,7 @@ services: **After:** ```yaml +# compose.yaml services: php: image: dunglas/frankenphp:1-php8.5 @@ -140,9 +148,9 @@ volumes: If you need additional PHP extensions, see [Building Custom Docker Image](docker.md#how-to-install-more-php-extensions). -For framework-specific Docker setups, see [Symfony Docker](https://github.com/dunglas/symfony-docker) and [Laravel](laravel.md#docker). +For framework-specific Docker setups, see [Symfony Docker](https://github.com/dunglas/symfony-docker) and [running Laravel with the FrankenPHP Docker image](laravel.md#running-laravel-with-the-frankenphp-docker-image). -## Step 5: Consider Worker Mode (Optional) +## Step 5: consider worker mode (optional) In [classic mode](classic.md), FrankenPHP works like PHP-FPM: each request boots the application from scratch. This is a safe starting point for migration. @@ -160,9 +168,9 @@ example.com { > [!CAUTION] > -> Worker mode keeps your application in memory between requests. Make sure your code does not rely on global state being reset between requests. Frameworks like [Symfony](worker.md#symfony-runtime), [Laravel](laravel.md#laravel-octane), and [API Platform](https://api-platform.com) have native support for this mode. +> Worker mode keeps your application in memory between requests. Make sure your code does not rely on global state being reset between requests. Frameworks like [Symfony](worker.md#worker-mode-for-symfony), [Laravel](laravel.md#laravel-octane), and [API Platform](https://api-platform.com) have native support for this mode. -## What You Can Remove +## What you can remove After migrating, you no longer need: diff --git a/docs/observability.md b/docs/observability.md index 0e58c07e9a..66508d15e8 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -1,9 +1,14 @@ +--- +title: FrankenPHP observability with metrics, logs, and Ember TUI +description: Monitor FrankenPHP in development and production using Prometheus metrics, structured logs, the Ember TUI dashboard, and custom Grafana scraping setups. +--- + # Observability FrankenPHP provides built-in observability features: [Prometheus-compatible metrics](metrics.md) and [structured logging](logging.md). These features, combined with the recommended tools below, give you full visibility into your PHP application's behavior in development and production. -## Ember TUI and Prometheus Exporter +## Ember TUI and Prometheus exporter [Ember](https://github.com/alexandre-daubois/ember) is the most user-friendly way to monitor FrankenPHP. @@ -26,7 +31,7 @@ FrankenPHP integrates with Caddy's logging system and provides `frankenphp_log() See the [Logging](logging.md) page for usage details. -## Custom Prometheus/Grafana Setup +## Custom Prometheus/Grafana setup If you prefer a custom monitoring stack, you can scrape FrankenPHP metrics directly. There are two options: diff --git a/docs/performance.md b/docs/performance.md index 5ca732d7f7..97e09e852e 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,9 +1,14 @@ +--- +title: FrankenPHP performance tuning guide +description: Tune FrankenPHP for higher throughput and lower latency: thread count, worker mode, glibc vs musl, Go runtime, OPcache, and Caddyfile options. +--- + # Performance By default, FrankenPHP tries to offer a good compromise between performance and ease of use. However, it is possible to substantially improve performance using an appropriate configuration. -## Number of Threads and Workers +## Tuning FrankenPHP threads and workers By default, FrankenPHP starts 2 times more threads and workers (in worker mode) than the available number of CPU cores. @@ -26,13 +31,13 @@ If set to `auto`, the limit will be estimated based on the `memory_limit` in you `max_threads` is similar to PHP FPM's [pm.max_children](https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-children). The main difference is that FrankenPHP uses threads instead of processes and automatically delegates them across different worker scripts and 'classic mode' as needed. -## Worker Mode +## Worker mode for higher throughput -Enabling [the worker mode](worker.md) dramatically improves performance, +Enabling [the FrankenPHP worker mode](worker.md) dramatically improves performance, but your app must be adapted to be compatible with this mode: you need to create a worker script and to be sure that the app is not leaking memory. -## Don't Use musl +## Avoid musl in production: prefer glibc builds The Alpine Linux variant of the official Docker images and the default binaries we provide are using [the musl libc](https://musl.libc.org). @@ -47,7 +52,7 @@ This can be achieved by using the Debian Docker images, using [our maintainers . For leaner or more secure containers, you may want to consider [a hardened Debian image](docker.md#hardening-images) rather than Alpine. -## Go Runtime Configuration +## Go runtime configuration for FrankenPHP FrankenPHP is written in Go. @@ -59,7 +64,7 @@ You likely want to set the `GODEBUG` environment variable to `cgocheck=0` (the d If you run FrankenPHP in containers (Docker, Kubernetes, LXC...) and limit the memory available for the containers, set the `GOMEMLIMIT` environment variable to the available amount of memory. -For more details, [the Go documentation page dedicated to this subject](https://pkg.go.dev/runtime#hdr-Environment_Variables) is a must-read to get the most out of the runtime. +For more details, [read the Go runtime environment variables reference](https://pkg.go.dev/runtime#hdr-Environment_Variables) to get the most out of the runtime. ## `file_server` @@ -107,6 +112,7 @@ files from PHP by path. This approach works well if your entire application is s An example [configuration](config.md#caddyfile-config) that serves static files behind an `/assets` folder could look like this: ```caddyfile +# Caddyfile: split static assets and PHP requests to skip filesystem lookups route { @assets { path /assets/* @@ -125,7 +131,7 @@ route { } ``` -## Placeholders +## Avoid Caddyfile placeholders in hot paths You can use [placeholders](https://caddyserver.com/docs/conventions#placeholders) in the `root` and `env` directives. However, this prevents caching these values, and comes with a significant performance cost. @@ -146,14 +152,14 @@ php_server { This will improve performance if the `root` directive contains [placeholders](https://caddyserver.com/docs/conventions#placeholders). The gain will be negligible in other cases. -## Logs +## FrankenPHP logging performance Logging is obviously very useful, but, by definition, it requires I/O operations and memory allocations, which considerably reduces performance. Make sure you [set the logging level](https://caddyserver.com/docs/caddyfile/options#log) correctly, and only log what's necessary. -## PHP Performance +## PHP performance tuning for FrankenPHP FrankenPHP uses the official PHP interpreter. All usual PHP-related performance optimizations apply with FrankenPHP. @@ -165,10 +171,10 @@ In particular: - ensure that the `realpath` cache is big enough for the needs of your application - use [preloading](https://www.php.net/manual/en/opcache.preloading.php) -For more details, read [the dedicated Symfony documentation entry](https://symfony.com/doc/current/performance.html) +For more details, read [the Symfony performance tuning documentation](https://symfony.com/doc/current/performance.html) (most tips are useful even if you don't use Symfony). -## Splitting The Thread Pool +## Splitting the FrankenPHP thread pool for slow endpoints It is common for applications to interact with slow external services, like an API that tends to be unreliable under high load or consistently takes 10+ seconds to respond. @@ -178,6 +184,7 @@ limits the concurrency of requests going towards the slow endpoint, similar to a connection pool. ```caddyfile +# Caddyfile: dedicated FrankenPHP thread pool for slow endpoints example.com { php_server { root /app/public # the root of your application diff --git a/docs/production.md b/docs/production.md index 7b6898d6f5..b47fdbe9f5 100644 --- a/docs/production.md +++ b/docs/production.md @@ -1,4 +1,9 @@ -# Deploying in Production +--- +title: Deploying FrankenPHP in production with Docker Compose +description: Deploy a PHP application to production with FrankenPHP and Docker Compose on a single Linux server, including TLS, reverse proxy, and multi-node setups. +--- + +# Deploying in production In this tutorial, we will learn how to deploy a PHP application on a single server using Docker Compose. @@ -6,7 +11,7 @@ If you're using Symfony, prefer reading the "[Deploy in production](https://gith If you're using API Platform (which also uses FrankenPHP), refer to [the deployment documentation of the framework](https://api-platform.com/docs/deployment/). -## Preparing Your App +## Preparing your app First, create a `Dockerfile` in the root directory of your PHP project: @@ -39,6 +44,7 @@ be sure to include it in the Docker image and to install your dependencies. Then, add a `compose.yaml` file: ```yaml +# compose.yaml services: php: image: dunglas/frankenphp @@ -68,13 +74,13 @@ volumes: Finally, if you use Git, commit these files and push. -## Preparing a Server +## Preparing a server To deploy your application in production, you need a server. In this tutorial, we will use a virtual machine provided by DigitalOcean, but any Linux server can work. -If you already have a Linux server with Docker installed, you can skip straight to [the next section](#configuring-a-domain-name). +If you already have a Linux server with Docker installed, you can skip straight to [Configuring a Domain Name](#configuring-a-domain-name). -Otherwise, use [this affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $200 of free credit, create an account, then click on "Create a Droplet". +Otherwise, use [this DigitalOcean affiliate link](https://m.do.co/c/5d8aabe3ab80) to get $200 of free credit, create an account, then click on "Create a Droplet". Then, click on the "Marketplace" tab under the "Choose an image" section and search for the app named "Docker". This will provision an Ubuntu server with the latest versions of Docker and Docker Compose already installed! @@ -93,7 +99,7 @@ When your Droplet is ready, use SSH to connect: ssh root@ ``` -## Configuring a Domain Name +## Configuring a domain name In most cases, you'll want to associate a domain name with your site. If you don't own a domain name yet, you'll have to buy one through a registrar. @@ -112,7 +118,7 @@ Example with the DigitalOcean Domains service ("Networking" > "Domains"): > > Let's Encrypt, the service used by default by FrankenPHP to automatically generate a TLS certificate doesn't support using bare IP addresses. Using a domain name is mandatory to use Let's Encrypt. -## Deploying +## Deploying FrankenPHP with Docker Compose Copy your project on the server using `git clone`, `scp`, or any other tool that may fit your need. If you use GitHub, you may want to use [a deploy key](https://docs.github.com/en/free-pro-team@latest/developers/overview/managing-deploy-keys#deploy-keys). @@ -137,7 +143,7 @@ Go to `https://your-domain-name.example.com` and enjoy! > > Docker can have a cache layer, make sure you have the right build for each deployment or rebuild your project with `--no-cache` option to avoid cache issue. -## Running Behind a Reverse Proxy +## Running behind a reverse proxy If FrankenPHP is running behind a reverse proxy or a load balancer (e.g., Nginx, AWS ELB, Google Cloud LB), you must configure the [`trusted_proxies` global option](https://caddyserver.com/docs/caddyfile/options#trusted-proxies) in your Caddyfile @@ -160,7 +166,7 @@ or the [`trustedproxies` middleware](https://laravel.com/docs/trustedproxy) for Without both configurations, headers such as `X-Forwarded-For` and `X-Forwarded-Proto` will be ignored, which can cause issues like incorrect HTTPS detection or wrong client IP addresses. -## Deploying on Multiple Nodes +## Deploying on multiple nodes If you want to deploy your app on a cluster of machines, you can use [Docker Swarm](https://docs.docker.com/engine/swarm/stack-deploy/), which is compatible with the provided Compose files. diff --git a/docs/static.md b/docs/static.md index 516acb3e91..635a6d8b0b 100644 --- a/docs/static.md +++ b/docs/static.md @@ -1,11 +1,16 @@ -# Create a Static Build +--- +title: FrankenPHP static build: single-binary PHP app server +description: Create static or mostly static FrankenPHP binaries with embedded PHP and Caddy on Linux and macOS, customize extensions, and load dynamic modules with glibc. +--- + +# Create a static build Instead of using a local installation of the PHP library, it's possible to create a static or mostly static build of FrankenPHP thanks to the great [static-php-cli project](https://github.com/crazywhalecc/static-php-cli) (despite its name, this project supports all SAPIs, not only CLI). With this method, a single, portable, binary will contain the PHP interpreter, the Caddy web server, and FrankenPHP! -Fully static native executables require no dependencies at all and can even be run on [`scratch` Docker image](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch). +Fully static native executables require no dependencies at all and can even be run on a [`scratch` Docker image](https://docs.docker.com/build/building/base-images/#create-a-minimal-base-image-using-scratch). However, they can't load dynamic PHP extensions (such as Xdebug) and have some limitations because they are using the musl libc. Mostly static binaries only require `glibc` and can load dynamic extensions. @@ -18,7 +23,7 @@ FrankenPHP also supports [embedding the PHP app in the static binary](embed.md). We provide Docker images to build static Linux binaries: -### musl-Based, Fully Static Build +### musl-based, fully static build For a fully-static binary that runs on any Linux distribution without dependencies but doesn't support dynamic loading of extensions: @@ -33,7 +38,7 @@ For better performance in heavily concurrent scenarios, consider using the [mima docker buildx bake --load --set static-builder-musl.args.MIMALLOC=1 static-builder-musl ``` -### glibc-Based, Mostly Static Build (With Dynamic Extension Support) +### glibc-based, mostly static build (with dynamic extension support) For a binary that supports loading PHP extensions dynamically while still having the selected extensions compiled statically: @@ -48,7 +53,7 @@ The resulting mostly static (except `glibc`) binary is named `frankenphp` and is If you want to build the static binary without Docker, take a look at the macOS instructions, which also work for Linux. -### Custom Extensions +### Custom PHP extensions in the static build By default, the most popular PHP extensions are compiled. @@ -71,7 +76,7 @@ docker buildx bake \ static-builder-musl ``` -### Extra Caddy Modules +### Extra Caddy modules To add extra Caddy modules or pass other arguments to [xcaddy](https://github.com/caddyserver/xcaddy), use the `XCADDY_ARGS` Docker ARG: @@ -89,9 +94,9 @@ In this example, we add the [Souin](https://souin.io) HTTP cache module for Cadd > The cbrotli, Mercure, and Vulcain modules are included by default if `XCADDY_ARGS` is empty or not set. > If you customize the value of `XCADDY_ARGS`, you must include them explicitly if you want them to be included. -See also how to [customize the build](#customizing-the-build) +See also how to [customize the FrankenPHP static build](#customizing-the-frankenphp-static-build) -### GitHub Token +### GitHub token If you hit the GitHub API rate limit, set a GitHub Personal Access Token in an environment variable named `GITHUB_TOKEN`: @@ -112,7 +117,7 @@ cd frankenphp Note: this script also works on Linux (and probably on other Unixes), and is used internally by the Docker images we provide. -## Customizing The Build +## Customizing the FrankenPHP static build The following environment variables can be passed to `docker build` and to the `build-static.sh` script to customize the static build: @@ -129,7 +134,7 @@ script to customize the static build: - `MIMALLOC`: (experimental, Linux-only) replace musl's mallocng by [mimalloc](https://github.com/microsoft/mimalloc) for improved performance. We only recommend using this for musl targeting builds, for glibc prefer disabling this option and using [`LD_PRELOAD`](https://microsoft.github.io/mimalloc/overrides.html) when you run your binary instead. - `RELEASE`: (maintainers only) when set, the resulting binary will be uploaded on GitHub -## Extensions +## Loading PHP extensions dynamically in the static binary With the glibc or macOS-based binaries, you can load PHP extensions dynamically. However, these extensions will have to be compiled with ZTS support. Since most package managers do not currently offer ZTS versions of their extensions, you will have to compile them yourself. diff --git a/docs/symfony.md b/docs/symfony.md index 7c41b49df9..4e9588c666 100644 --- a/docs/symfony.md +++ b/docs/symfony.md @@ -1,10 +1,15 @@ +--- +title: Running Symfony with FrankenPHP (Docker, worker mode, hot reload) +description: How to run a Symfony application with FrankenPHP using Symfony Docker, a local install, worker mode, hot reload, AssetMapper, and X-Sendfile. +--- + # Symfony -## Docker +## Running Symfony with Symfony Docker For [Symfony](https://symfony.com) projects, we recommend using [Symfony Docker](https://github.com/dunglas/symfony-docker), the official Symfony Docker setup maintained by FrankenPHP's author. It provides a complete Docker-based environment with FrankenPHP, automatic HTTPS, HTTP/2, HTTP/3, and worker mode support out of the box. -## Local Installation +## Installing Symfony with FrankenPHP locally Alternatively, you can run your Symfony projects with FrankenPHP from your local machine: @@ -12,6 +17,7 @@ Alternatively, you can run your Symfony projects with FrankenPHP from your local 2. Add the following configuration to a file named `Caddyfile` in the root directory of your Symfony project: ```caddyfile + # Caddyfile # The domain name of your server localhost @@ -26,7 +32,7 @@ Alternatively, you can run your Symfony projects with FrankenPHP from your local 3. Start FrankenPHP from the root directory of your Symfony project: `frankenphp run` -## Worker Mode +## Symfony worker mode with FrankenPHP Since Symfony 7.4, FrankenPHP worker mode is natively supported. @@ -49,7 +55,7 @@ docker run \ Learn more about [the worker mode](worker.md). -### Auditing Worker Compatibility +### Auditing worker compatibility [Igor PHP](https://github.com/igor-php/igor-php) is a static linter that scans Symfony projects for state leaks before they bite in production: services missing `ResetInterface`, stateful properties that aren't reset, mutable local statics, `exit()`/`die()` calls, and superglobal writes. It audits your application code as well as services declared in `vendor/`. @@ -58,7 +64,7 @@ composer require --dev igor-php/igor-php vendor/bin/igor-php . ``` -## Hot Reload +## Hot reload for Symfony Hot reloading is enabled by default in [Symfony Docker](https://github.com/dunglas/symfony-docker). @@ -81,6 +87,7 @@ php_server { Then, add the following code to your `templates/base.html.twig` file: ```twig +{# templates/base.html.twig #} {% if app.request.server.has('FRANKENPHP_HOT_RELOAD') %} @@ -90,7 +97,7 @@ Then, add the following code to your `templates/base.html.twig` file: Finally, run `frankenphp run` from the root directory of your Symfony project. -## Pre-Compressing Assets +## Pre-compressing assets Symfony's [AssetMapper component](https://symfony.com/doc/current/frontend/asset_mapper.html) can pre-compress assets with Brotli and Zstandard during deployment. FrankenPHP (through Caddy's `file_server`) can serve these pre-compressed files directly, avoiding on-the-fly compression overhead. @@ -103,6 +110,7 @@ Symfony's [AssetMapper component](https://symfony.com/doc/current/frontend/asset 2. Update your `Caddyfile` to serve pre-compressed assets: ```caddyfile + # Caddyfile localhost @assets path /assets/* @@ -118,12 +126,12 @@ Symfony's [AssetMapper component](https://symfony.com/doc/current/frontend/asset The `precompressed` directive tells Caddy to look for pre-compressed versions of the requested file (e.g., `app.css.zst`, `app.css.br`) and serve them directly if the client supports it. -## Serving Large Static Files (`X-Sendfile`) +## Serving large static files (`X-Sendfile`) FrankenPHP supports [efficiently serving large static files](x-sendfile.md) after executing PHP code (for access control, statistics, etc.). Symfony HttpFoundation [natively supports this feature](https://symfony.com/doc/current/components/http_foundation.html#serving-files). -After [configuring your `Caddyfile`](x-sendfile.md#configuration), it will automatically determine the correct value for the `X-Accel-Redirect` header and add it to the response: +After [configuring your `Caddyfile`](x-sendfile.md#configuring-x-accel-redirect-in-the-frankenphp-caddyfile), it will automatically determine the correct value for the `X-Accel-Redirect` header and add it to the response: ```php use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -134,7 +142,7 @@ $response = new BinaryFileResponse(__DIR__.'/../private-files/file.txt'); // ... ``` -## Symfony Apps As Standalone Binaries +## Symfony apps as standalone binaries Using [FrankenPHP's application embedding feature](embed.md), it's possible to distribute Symfony apps as standalone binaries. @@ -167,6 +175,7 @@ Follow these steps to prepare and package your Symfony app: 2. Create a file named `static-build.Dockerfile` in the repository of your app: ```dockerfile + # static-build.Dockerfile FROM --platform=linux/amd64 dunglas/frankenphp:static-builder-gnu # If you intend to run the binary on musl-libc systems, use static-builder-musl instead diff --git a/docs/wordpress.md b/docs/wordpress.md index bc9f2756fc..c762cc07da 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -1,8 +1,13 @@ +--- +title: Running WordPress with FrankenPHP (HTTPS, HTTP/3, hot reload) +description: How to run WordPress with FrankenPHP, including a minimal install, a production Caddyfile, and enabling hot reload via Mercure. +--- + # WordPress Run [WordPress](https://wordpress.org/) with FrankenPHP to enjoy a modern, high-performance stack with automatic HTTPS, HTTP/3, and Zstandard compression. -## Minimal Installation +## Installing WordPress with FrankenPHP 1. [Download WordPress](https://wordpress.org/download/) 2. Extract the ZIP archive and open a terminal in the extracted directory @@ -25,7 +30,7 @@ encode zstd br gzip log ``` -## Hot Reload +## Hot reload for WordPress To use the [hot reload](hot-reload.md) feature with WordPress, enable [Mercure](mercure.md) and add the `hot_reload` sub-directive to the `php_server` directive in your `Caddyfile`: @@ -44,6 +49,7 @@ php_server { Then, add the code needed to load the JavaScript libraries in the `functions.php` file of your WordPress theme: ```php +// wp-content/themes//functions.php function hot_reload() { ?> diff --git a/docs/worker.md b/docs/worker.md index 30cc0cfa73..cd78811a28 100644 --- a/docs/worker.md +++ b/docs/worker.md @@ -1,15 +1,20 @@ -# Using FrankenPHP Workers +--- +title: FrankenPHP worker mode: keep your PHP app in memory +description: Run FrankenPHP in worker mode to keep your PHP application bootstrapped between requests, cut bootstrap overhead, and serve responses in milliseconds. +--- + +# Using FrankenPHP workers Boot your application once and keep it in memory. FrankenPHP will handle incoming requests in a few milliseconds. -## Starting Worker Scripts +## Starting FrankenPHP worker scripts -### Docker +### Running a FrankenPHP worker with Docker Set the value of the `FRANKENPHP_CONFIG` environment variable to `worker /path/to/your/worker/script.php`: -```console +```bash docker run \ -e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \ -v $PWD:/app \ @@ -17,11 +22,11 @@ docker run \ dunglas/frankenphp ``` -### Standalone Binary +### Running a FrankenPHP worker with the standalone binary Use the `--worker` option of the `php-server` command to serve the content of the current directory using a worker: -```console +```bash frankenphp php-server --worker /path/to/your/worker/script.php ``` @@ -31,21 +36,21 @@ It will be used automatically. It's also possible to [restart the worker on file changes](config.md#watching-for-file-changes) with the `--watch` option. The following command will trigger a restart if any file ending in `.php` in the `/path/to/your/app/` directory or subdirectories is modified: -```console +```bash frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php" ``` This feature is often used in combination with [hot reloading](hot-reload.md). -## Symfony +## Worker mode for Symfony -See [the dedicated documentation](symfony.md#worker-mode). +See [the FrankenPHP Symfony worker mode documentation](symfony.md#symfony-worker-mode-with-frankenphp). -## Laravel Octane +## Worker mode for Laravel Octane -See [the dedicated documentation](laravel.md#laravel-octane). +See [the FrankenPHP Laravel Octane documentation](laravel.md#laravel-octane). -## Custom Apps +## Writing a custom FrankenPHP worker script The following example shows how to create your own worker script without relying on a third-party library: @@ -91,7 +96,7 @@ $myApp->shutdown(); Then, start your app and use the `FRANKENPHP_CONFIG` environment variable to configure your worker: -```console +```bash docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php" \ -v $PWD:/app \ @@ -102,7 +107,7 @@ docker run \ By default, 2 workers per CPU are started. You can also configure the number of workers to start: -```console +```bash docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php 42" \ -v $PWD:/app \ @@ -110,24 +115,24 @@ docker run \ dunglas/frankenphp ``` -### Restart the Worker After a Certain Number of Requests +### Restart the worker after a certain number of requests As PHP was not originally designed for long-running processes, there are still many libraries and legacy codes that leak memory. A workaround to using this type of code in worker mode is to restart the worker script after processing a certain number of requests: -The previous worker snippet allows configuring a maximum number of request to handle by setting an environment variable named `MAX_REQUESTS`. +The previous worker snippet allows configuring a maximum number of requests to handle by setting an environment variable named `MAX_REQUESTS`. -### Restart Workers Manually +### Restart workers manually While it's possible to restart workers [on file changes](config.md#watching-for-file-changes), it's also possible to restart all workers gracefully via the [Caddy admin API](https://caddyserver.com/docs/api). If the admin is enabled in your [Caddyfile](config.md#caddyfile-config), you can ping the restart endpoint with a simple POST request like this: -```console +```bash curl -X POST http://localhost:2019/frankenphp/workers/restart ``` -### Worker Failures +### Worker failures If a worker script crashes with a non-zero exit code, FrankenPHP will restart it with an exponential backoff strategy. If the worker script stays up longer than the last backoff × 2, @@ -146,7 +151,7 @@ frankenphp { } ``` -## Superglobals Behavior +## Superglobals behavior [PHP superglobals](https://www.php.net/manual/en/language.variables.superglobals.php) (`$_SERVER`, `$_ENV`, `$_GET`...) behave as follows: @@ -174,7 +179,7 @@ However, **`$_ENV` is currently not reset between requests**. This means that any modifications made to `$_ENV` during a request will persist and be visible to subsequent requests handled by the same worker thread. Avoid storing request-specific or sensitive data in `$_ENV`. -## State Persistence +## State persistence Because worker mode keeps the PHP process alive between requests, the following state persists across requests: diff --git a/docs/x-sendfile.md b/docs/x-sendfile.md index f9513c346d..2a29b2a988 100644 --- a/docs/x-sendfile.md +++ b/docs/x-sendfile.md @@ -1,4 +1,9 @@ -# Efficiently Serving Large Static Files (`X-Sendfile`/`X-Accel-Redirect`) +--- +title: Serving large files with X-Sendfile and X-Accel-Redirect in FrankenPHP +description: Configure FrankenPHP to delegate large static file delivery to the web server after running PHP code, using X-Sendfile or X-Accel-Redirect headers. +--- + +# Efficiently serving large static files (`X-Sendfile`/`X-Accel-Redirect`) Usually, static files can be served directly by the web server, but sometimes it's necessary to execute some PHP code before sending them: @@ -15,11 +20,11 @@ containing the path of the file to be served. FrankenPHP takes care of the rest. This feature is known as **`X-Sendfile`** for Apache, and **`X-Accel-Redirect`** for NGINX. -In the following examples, we assume that the document root of the project is the `public/` directory. +In the following examples, we assume that the document root of the project is the `public/` directory, and that we want to use PHP to serve files stored outside the `public/` directory, from a directory named `private-files/`. -## Configuration +## Configuring X-Accel-Redirect in the FrankenPHP Caddyfile First, add the following configuration to your `Caddyfile` to enable this feature: diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000000..c0a3fde949 --- /dev/null +++ b/llms.txt @@ -0,0 +1,56 @@ +# FrankenPHP + +> FrankenPHP is a modern PHP application server written in Go and powered by [Caddy](https://caddyserver.com). It runs PHP applications with automatic HTTPS, HTTP/2 and HTTP/3, an in-memory worker mode, real-time updates via [Mercure](https://mercure.rocks), hot reload, and a static-binary deployment story. + +This index points AI agents and crawlers at the canonical English documentation. All URLs are absolute. Each Markdown source file carries SEO and LLM-friendly YAML frontmatter (`title`, `description`). + +## Core concepts + +- [FrankenPHP Classic Mode](https://frankenphp.dev/docs/classic/): drop-in replacement for PHP-FPM and Apache mod_php using a fixed or autoscaling thread pool. +- [FrankenPHP Worker Mode](https://frankenphp.dev/docs/worker/): keep your PHP application bootstrapped in memory between requests for lower latency. +- [Configuring FrankenPHP](https://frankenphp.dev/docs/config/): Caddyfile, JSON, environment variables, and `php.ini` configuration. +- [FrankenPHP Internals](https://frankenphp.dev/docs/internals/): thread types, the state machine, the Go/C/PHP CGO boundary, auto-scaling, and per-thread environment sandboxing. + +## Setup and build + +- [FrankenPHP Docker Image](https://frankenphp.dev/docs/docker/): build custom images, install PHP extensions and Caddy modules, run as non-root, harden with distroless. +- [Compile FrankenPHP From Sources](https://frankenphp.dev/docs/compile/): build on Linux, macOS and FreeBSD with PHP linked as a dynamic library. +- [Static Build](https://frankenphp.dev/docs/static/): create a single self-contained FrankenPHP binary with embedded PHP and Caddy. +- [Embedding PHP Apps as Standalone Binaries](https://frankenphp.dev/docs/embed/): ship a pure-PHP, Symfony or Laravel app as one static executable. +- [Building Images with GitHub Actions](https://frankenphp.dev/docs/github-actions/): automate Docker image builds and publication. + +## Worker mode and extensions + +- [FrankenPHP Worker Mode](https://frankenphp.dev/docs/worker/): supervisor configuration, restart policies, and integration with Symfony Runtime and Laravel Octane. +- [Extension Workers](https://frankenphp.dev/docs/extension-workers/): dedicated PHP thread pool from a Go extension for queues, schedulers, and event listeners. +- [Writing PHP Extensions in Go](https://frankenphp.dev/docs/extensions/): extension generator, types API, calling PHP from Go, classes, methods, and constants. + +## Frameworks + +- [Laravel with FrankenPHP](https://frankenphp.dev/docs/laravel/): Docker, local install, Laravel Octane integration, and standalone binaries. +- [Symfony with FrankenPHP](https://frankenphp.dev/docs/symfony/): Symfony Docker, worker mode, hot reload, AssetMapper, and X-Sendfile. +- [WordPress with FrankenPHP](https://frankenphp.dev/docs/wordpress/): minimal install, production Caddyfile, hot reload via Mercure. +- [Migrating from Nginx and PHP-FPM](https://frankenphp.dev/docs/migrate/): step-by-step migration of an existing PHP stack. + +## Real-time and performance features + +- [Mercure Real-Time Hub](https://frankenphp.dev/docs/mercure/): built-in Mercure hub for pushing real-time events to browsers over HTTP. +- [HTTP 103 Early Hints](https://frankenphp.dev/docs/early-hints/): preload critical assets before the final response. +- [Hot Reload](https://frankenphp.dev/docs/hot-reload/): refresh PHP, templates, and frontend assets without manual reload. +- [X-Sendfile and X-Accel-Redirect](https://frankenphp.dev/docs/x-sendfile/): delegate large file delivery to the web server after running PHP. +- [Performance Tuning](https://frankenphp.dev/docs/performance/): thread count, worker mode, glibc vs musl, Go runtime, OPcache, Caddyfile options. + +## Production and observability + +- [Deploying FrankenPHP in Production](https://frankenphp.dev/docs/production/): Docker Compose deployments with TLS, reverse proxy, and multi-node setups. +- [Known Issues](https://frankenphp.dev/docs/known-issues/): unsupported PHP extensions, musl caveats, Docker TLS gotchas. +- [Observability Overview](https://frankenphp.dev/docs/observability/): Prometheus metrics, structured logs, and the Ember TUI dashboard. +- [Prometheus Metrics](https://frankenphp.dev/docs/metrics/): exposed metrics for threads, workers, queues, crashes, and restarts. +- [Logging](https://frankenphp.dev/docs/logging/): structured logging with `frankenphp_log()` and `error_log()` routed through Caddy. + +## Project + +- [GitHub Repository](https://github.com/php/frankenphp) +- [Docker Hub](https://hub.docker.com/r/dunglas/frankenphp) +- [README](https://github.com/php/frankenphp/blob/main/README.md) +- [Contributing Guide](https://github.com/php/frankenphp/blob/main/CONTRIBUTING.md)