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
- What runs your application
- The three files
- docker/Caddyfile
- Example: security headers
- Example: cache your own images longer, and close a folder
- docker/php.ini
- Example: larger uploads and more memory
- docker/entrypoint.sh
- Example: one step more
- Worker mode
- When a release does not go live after a change
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,exifand OPcache. Also on the image:pg_dumpandpg_restorefor your own logical back-ups, andpdftotext. - Your code is in
/app, and the application runs as the userwww-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:
{
# 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.frankenphpandphp_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.eutoexample.euturns each of those calls into a redirect, and so into a failure. A rule for one name, such aswww.example.eutoexample.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
{
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:
; 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 = EGPCSmakes your variables reach the application. Without theE,$_ENVstays empty.opcache.validate_timestamps = 0is 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.*andfatal_error_backtracesare security settings. Leave them as they are.
Example: larger uploads and more memory
; ... 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:
#!/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:
webends inexec frankenphp run --config /etc/frankenphp/Caddyfile. Theexecmatters: 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 ... :cachelines. 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.