octane
octane copied to clipboard
FrankenPHP: Allow more control over which environment variables are delegated to frankenphp process
Problem
We are deploying FrankenPHP + Octane and use a custom Caddyfile to proxy the Reverb Server and provide additional Caddy Modules and configuration.
Currently, the frankenphp runtime is hardcoded and limited to these environment variables:
src/Commands/StartFrankenPhpCommand.php
'APP_ENV' => app()->environment(),
'APP_BASE_PATH' => base_path(),
'APP_PUBLIC_PATH' => public_path(),
'LARAVEL_OCTANE' => 1,
'MAX_REQUESTS' => $this->option('max-requests'),
'REQUEST_MAX_EXECUTION_TIME' => $this->maxExecutionTime(),
'CADDY_GLOBAL_OPTIONS' => ($https && $this->option('http-redirect')) ? '' : 'auto_https disable_redirects',
'CADDY_SERVER_ADMIN_PORT' => $this->adminPort(),
'CADDY_SERVER_LOG_LEVEL' => $this->option('log-level') ?: (app()->environment('local') ? 'INFO' : 'WARN'),
'CADDY_SERVER_LOGGER' => 'json',
'CADDY_SERVER_SERVER_NAME' => $serverName,
'CADDY_SERVER_WORKER_COUNT' => $this->workerCount() ?: '',
'CADDY_SERVER_EXTRA_DIRECTIVES' => $this->buildMercureConfig(),
Due to the vast amount of flexibilty and usecases the FrankenPHP variant provides, I think it would be helpful to allow integrators a way to manipulate or hook into these environment variables, maybe even just by moving these to a dedicated method.
Example
To extend Caddy, we incoorperate the Octane Caddyfile into our own along these lines:
{
{$CADDY_GLOBAL_OPTIONS}
admin localhost:{$CADDY_SERVER_ADMIN_PORT}
order php_server before file_server
order php before file_server
exec php artisan deploy
frankenphp {
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
}
supervisor {
php /app/artisan queue:work
php /app/artisan horizon
php /app/artisan pulse:check
php /app/artisan pulse:work
php /app/artisan reverb:start
supercronic /etc/supercronic/laravel
}
}
{$CADDY_SERVER_SERVER_NAME} {
log {
level {$CADDY_SERVER_LOG_LEVEL}
# Redact the authorization query parameter that can be set by Mercure...
format filter {
wrap {$CADDY_SERVER_LOGGER}
fields {
uri query {
replace authorization REDACTED
}
}
}
}
route {
root * "{$APP_PUBLIC_PATH}"
encode zstd br gzip
# Mercure configuration is injected here...
{$CADDY_SERVER_EXTRA_DIRECTIVES}
php_server {
index frankenphp-worker.php
# Required for the public/storage/ directory...
resolve_root_symlink
}
}
}
{$REVERB_HOST} {
reverse_proxy ${REVERB_SERVER_HOST}:{$REVERB_SERVER_PORT}
}
This is a straightforward way to proxy REVERB given the known environment variables that are already present in the container.
To work around this, we currently have to provide our own octane:frankenphp Command, here as app:start
<?php
namespace App\Console\Commands;
use Laravel\Octane\Commands\StartFrankenPhpCommand;
use Symfony\Component\Console\Attribute\AsCommand;
use Laravel\Octane\FrankenPhp\ServerProcessInspector;
use Laravel\Octane\FrankenPhp\ServerStateFile;
#[AsCommand(name: 'app:start')]
class StartAppCommand extends StartFrankenPhpCommand
{
public $signature = 'app:start
{--host=127.0.0.1 : The IP address the server should bind to}
{--port= : The port the server should be available on}
{--admin-port= : The port the admin server should be available on}
{--workers=auto : The number of workers that should be available to handle requests}
{--max-requests=500 : The number of requests to process before reloading the server}
{--caddyfile= : The path to the FrankenPHP Caddyfile file}
{--https : Enable HTTPS, HTTP/2, and HTTP/3, and automatically generate and renew certificates}
{--http-redirect : Enable HTTP to HTTPS redirection (only enabled if --https is passed)}
{--watch : Automatically reload the server when the application is modified}
{--poll : Use file system polling while watching in order to watch files over a network}
{--log-level= : Log messages at or above the specified log level}';
public function handle(ServerProcessInspector $inspector, ServerStateFile $serverStateFile)
{
$this->ensureFrankenPhpWorkerIsInstalled();
$this->ensurePortIsAvailable();
$frankenphpBinary = $this->ensureFrankenPhpBinaryIsInstalled();
if ($inspector->serverIsRunning()) {
$this->error('FrankenPHP server is already running.');
return 1;
}
$this->ensureFrankenPhpBinaryMeetsRequirements($frankenphpBinary);
$this->writeServerStateFile($serverStateFile);
$this->forgetEnvironmentVariables();
$host = $this->option('host');
$port = $this->getPort();
$https = $this->option('https');
$serverName = $https
? "https://$host:$port"
: "http://:$port";
$process = tap(new Process([
$frankenphpBinary,
'run',
'-c', $this->configPath(),
], base_path(), [
'APP_ENV' => app()->environment(),
'APP_BASE_PATH' => base_path(),
'APP_PUBLIC_PATH' => public_path(),
'LARAVEL_OCTANE' => 1,
'MAX_REQUESTS' => $this->option('max-requests'),
'REQUEST_MAX_EXECUTION_TIME' => $this->maxExecutionTime(),
'CADDY_GLOBAL_OPTIONS' => ($https && $this->option('http-redirect')) ? '' : 'auto_https disable_redirects',
'CADDY_SERVER_ADMIN_PORT' => $this->adminPort(),
'CADDY_SERVER_LOG_LEVEL' => $this->option('log-level') ?: (app()->environment('local') ? 'INFO' : 'WARN'),
'CADDY_SERVER_LOGGER' => 'json',
'CADDY_SERVER_SERVER_NAME' => $serverName,
'CADDY_SERVER_WORKER_COUNT' => $this->workerCount() ?: '',
'CADDY_SERVER_EXTRA_DIRECTIVES' => $this->buildMercureConfig(),
// Additional variables
'REVERB_SERVER_HOST' => env('REVERB_SERVER_HOST'),
'REVERB_SERVER_PORT' => env('REVERB_SERVER_PORT'),
'REVERB_HOST' => env('REVERB_HOST'),
]));
$server = $process->start();
$serverStateFile->writeProcessId($server->getPid());
return $this->runServer($server, $inspector, 'frankenphp');
}
}
I wonder what you or @dunglas advise in this situation, since our hacky solution is not really something we love.