Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
PascaleBeier opened this issue May 18, 2024 · 3 comments

Comments

@PascaleBeier
Copy link

PascaleBeier commented May 18, 2024

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.

@PascaleBeier PascaleBeier changed the title Frankenphp: Allow more control over which environment variables are delegated to frankenphp process FrankenPHP: Allow more control over which environment variables are delegated to frankenphp process May 18, 2024
@driesvints
Copy link
Member

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

@dunglas
Copy link
Contributor

dunglas commented May 21, 2024

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
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants