laravel / octane

Supercharge your Laravel application's performance.
https://laravel.com/docs/octane
MIT License
3.71k stars 284 forks source link

FrankenPHP: Allow more control over which environment variables are delegated to frankenphp process #895

Closed PascaleBeier closed 1 month ago

PascaleBeier commented 1 month ago

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.

driesvints commented 1 month ago

@dunglas could you give your 2 cents here? Thanks!

dunglas commented 1 month ago

The FrankenPHP process started by Symfony Process inherits env vars defined in the system. I didn't try, but calling putenv('FOO=BAR') should be enough to pass FOO to FrankenPHP.

That being said, I'm +1 to add an extension point allowing users to customize env vars passed to FrankenPHP.

driesvints commented 1 month ago

Thanks @dunglas. In that regard @PascaleBeier we'd appreciate a PR we could have a look at, thanks.