Brewless Brewless Docs

Deployments

Server configuration

What serves your application, and how to change it with your own docker/Caddyfile, docker/php.ini and docker/entrypoint.sh.

On this page

Brewless builds every release on its public base image and runs it on FrankenPHP. Most applications need nothing on this page. When yours needs other headers, other PHP settings or a step before it starts, three files in your repository change how it is served.

What runs your application

Every release is one image, built on ghcr.io/brewless/brewless-base:php<version> for the PHP version you chose. The image and its defaults are open source, in the images repository.

  • FrankenPHP in classic mode. FrankenPHP is a PHP application server built on the Caddy web server. In classic mode every request starts with a clean state, as with PHP-FPM, so an application that runs on PHP-FPM runs here as it is.
  • Extensions: pdo_pgsql, pdo_mysql, redis, intl, bcmath, pcntl, zip, gd, exif and OPcache. Also on the image: pg_dump and pg_restore for your own logical back-ups, and pdftotext.
  • Your code is in /app, and the application runs as the user www-data. The web root is /app/public.
  • The port is the one Scaleway gives in PORT (8080). HTTPS ends before the container: at bunny.net, or at Scaleway without bunny.net. The container only speaks plain HTTP to them.

All three containers of an environment run this image, and all three start the same way, as a web server:

Container Who calls it What it answers
web Your visitors, through bunny.net or Scaleway Your application
release Brewless, on a deploy /_ops/...: the release commands, such as your migrations
worker A trigger of Scaleway, every minute A POST to /: one tick of the schedule and the queue

The release and worker routes come from the brewless/laravel package. That matters for what follows: your server configuration is the configuration of all three containers.

The three files

Commit any of these files and the next build puts it in the image in place of the default of that name.

File Replaces Use it for
docker/Caddyfile /etc/frankenphp/Caddyfile, the server configuration Response headers, caching rules, compression, blocking a path
docker/php.ini /usr/local/etc/php/conf.d/zz-brewless.ini, Brewless's PHP settings Memory limit, upload size, OPcache and other PHP settings
docker/entrypoint.sh /usr/local/bin/brewless-entrypoint, the start script Work before the server starts

Each file replaces its default as a whole: nothing is merged. Copy the default below, then change what you need. A file you remove from the repository brings the default back with the next deploy.

The files are taken when the image is built, so a change reaches the containers with the next deploy, never in between. A rollback puts back the image of that release, and so the files it was built with.

docker/Caddyfile

The default:

docker/Caddyfile
{
	# TLS ends at the edge and at the provider's own endpoint.
	auto_https off
	admin off

	frankenphp
}

:{$PORT:8080} {
	root * /app/public
	encode zstd br gzip

	# Fingerprinted build output can be cached for a year.
	@build path /build/*
	header @build Cache-Control "public, max-age=31536000, immutable"

	php_server
}

What your own version has to keep:

  • :{$PORT:8080} as the address. Scaleway sends traffic to that port; a server on another port is never reached, and the container does not go live.
  • auto_https off. There is no certificate to get in the container, and Caddy trying one would only delay the start.
  • frankenphp and php_server. Without them nothing runs your PHP.
  • root * /app/public, unless your application serves from another folder.

What it must not do:

  • Answer / or /_ops/... itself, with a redirect, a static file or an error. The worker's trigger posts to / and the release commands go to /_ops/...: both have to reach PHP, or the schedule, the queue and your migrations stop running.
  • Redirect every other hostname to one domain. The worker and release containers are only ever called on their own address at Scaleway, and without bunny.net Brewless checks the web container's health on its address too. A rule that sends any hostname but example.eu to example.eu turns each of those calls into a redirect, and so into a failure. A rule for one name, such as www.example.eu to example.eu, is fine. Better still, make it in your application, which knows its domains.
  • Ask for a password on everything. The same reason: the trigger and Brewless cannot give one. Protect a path, not the whole site.

Example: security headers

docker/Caddyfile
{
	auto_https off
	admin off

	frankenphp
}

:{$PORT:8080} {
	root * /app/public
	encode zstd br gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}

	@build path /build/*
	header @build Cache-Control "public, max-age=31536000, immutable"

	php_server
}

Example: cache your own images longer, and close a folder

	@images path /images/*
	header @images Cache-Control "public, max-age=604800"

	@private path /storage/private/*
	respond @private 404

These lines go inside the :{$PORT:8080} block, before php_server. The Caddyfile documentation and FrankenPHP's own describe every directive.

docker/php.ini

The default:

docker/php.ini
; Brewless runtime settings.
memory_limit = 256M
expose_php = Off
variables_order = EGPCS

opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 128
opcache.max_accelerated_files = 20000
; The code in an image never changes, so there is nothing to revalidate.
opcache.validate_timestamps = 0

session.upload_progress.enabled = Off
session.use_strict_mode = 1
; PHP 8.5 only, ignored by older versions.
fatal_error_backtraces = Off

Your file replaces all of it, so keep the lines you do not mean to change:

  • variables_order = EGPCS makes your variables reach the application. Without the E, $_ENV stays empty.
  • opcache.validate_timestamps = 0 is safe because the code in an image never changes; it saves a look at the disk on every file of every request.
  • expose_php, session.* and fatal_error_backtraces are security settings. Leave them as they are.

Example: larger uploads and more memory

docker/php.ini
; ... every line of the default, then:
memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 64M

memory_limit is per request, inside the memory of the container. With 512 MB for the web container and up to 50 requests at once, a limit of 512M per request lets a few heavy requests take all of it. Raise the container's memory in brewless.yml together with this limit; see Scaling.

docker/entrypoint.sh

The default:

docker/entrypoint.sh
#!/bin/sh
set -e

# One image, several roles. The role is the first argument:
#   web        serve HTTP (default)
#   worker     process the queue
#   scheduler  run the scheduler once, for a cron trigger
#   anything else is passed to the application's console

cd /app

if [ -f artisan ]; then
    # Configuration comes from the environment of this container, so it can
    # only be cached here, not at build time.
    php artisan config:cache --no-interaction >/dev/null
    php artisan route:cache --no-interaction >/dev/null
    php artisan view:cache --no-interaction >/dev/null
fi

case "${1:-web}" in
    web)
        exec frankenphp run --config /etc/frankenphp/Caddyfile
        ;;
    worker)
        shift
        exec php artisan queue:work --no-interaction --tries=3 --max-time=3300 "$@"
        ;;
    scheduler)
        exec php artisan schedule:run --no-interaction
        ;;
    *)
        exec php artisan "$@"
        ;;
esac

Brewless starts every container with web; the other roles are there for when you run the image yourself. What your version has to keep:

  • web ends in exec frankenphp run --config /etc/frankenphp/Caddyfile. The exec matters: it makes FrankenPHP the process that Scaleway stops and starts.
  • set -e, so a step that fails stops the start where you can see it, instead of serving an application that is half ready.
  • The three artisan ... :cache lines. They run on every start because your variables only exist in the running container. Without them every request reads your configuration and routes from scratch.

Everything in this script runs before the first request is answered, on every start. A container that was scaled to zero starts when a request comes in, so each second here is a second that visitor waits. Keep it to work that takes moments, and leave migrations out: Brewless runs your release commands once per deploy, on the release container.

Example: one step more

if [ -f artisan ]; then
    php artisan config:cache --no-interaction >/dev/null
    php artisan route:cache --no-interaction >/dev/null
    php artisan view:cache --no-interaction >/dev/null
    php artisan icons:cache --no-interaction >/dev/null
fi

The file has to be executable in the image; the build sets that, whatever its mode in your repository.

Worker mode

FrankenPHP can also run in worker mode: the application boots once and stays in memory between requests, which makes each request faster. Laravel Octane uses it. Brewless does not set it up and does not support it.

  • State stays. What one request leaves in a static property or a singleton, the next request finds. With a multi-tenant package such as stancl/tenancy, whose maintainer does not support Octane, that can show one tenant's data to another.
  • It does not prevent cold starts. A container that was scaled to zero still has to start; worker mode only helps once it runs.

A docker/Caddyfile can switch worker mode on. If you do, you own what it does: your code, and every package in it, has to be written for a process that outlives a request.

When a release does not go live after a change

  • The container does not start. The log of the web container says why; see Logs. A Caddyfile with an error, or an entrypoint that exits, stops the start. Remove the file, or fix it, and deploy again.
  • The health check fails. Look for a redirect of every other hostname, a rule on /up, or a password on the whole site.
  • Migrations or the schedule stop running. Look for a rule that answers / or /_ops/... before PHP does.

The release that served visitors keeps doing so until the new one is healthy, so a mistake in these files never takes the application down.