Dockerfile cookbook¶
Recipes for the images shipped by the plugin. They all assume a
Dockerfile next to your castor.php, registered on the service:
(new SymfonyService('app'))
->withDirectory(__DIR__)
->withDockerfile(__DIR__ . '/Dockerfile')
and a castor docker:build after every change. The mechanism behind them is
described in custom Dockerfile, and the blocks and
variables in PHP and
Symfony.
Examples extend Dockerfile.frankenphp, the default mode. In PhpMode::Fpm,
extend Dockerfile instead — the block names are the same.
A system package in every container¶
php_base is the shared ancestor of the builder and the workers, and of the
frontend in PhpMode::Fpm:
# syntax=ghcr.io/castor-php/twig-dockerfile:0.1
{% extends 'Dockerfile.frankenphp' %}
{% block php_base %}
{{ parent() }}
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
poppler-utils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
{% endblock %}
In FrankenPHP mode the frontend comes from dunglas/frankenphp and does not
inherit this. Repeat the step in frontend when the web container needs the
package too — that image is Debian-based as well, so the same apt-get works.
A tool only the builder needs¶
Compilers, linters and CLIs belong to builder: the container you get with
castor app:bash, and the one your CI runs. Keeping them out of php_base
keeps the runtime images small.
{% block builder %}
{{ parent() }}
RUN curl -sS https://get.symfony.com/cli/installer | bash -s -- --install-dir=/usr/local/bin
{% endblock %}
PHP settings¶
Drop an ini file in mods-available and enable it. The priority comment orders
it against the ones the plugin ships (app-default is 30, app-builder 40):
{% block php_base %}
{{ parent() }}
COPY <<EOF /etc/php/{{ php_version }}/mods-available/project.ini
; priority=50
[PHP]
memory_limit = 512M
max_execution_time = 60
EOF
RUN phpenmod project
{% endblock %}
The FrankenPHP frontend uses the upstream image layout instead, with no
phpenmod:
{% block frontend %}
{{ parent() }}
COPY <<EOF /usr/local/etc/php/conf.d/zz-project.ini
memory_limit = 512M
EOF
{% endblock %}
The fake sudo in the builder¶
The shipped Dockerfile carries this step commented out. Enabling it lets you
sudo inside the builder container, which is convenient while debugging:
{% block builder %}
{{ parent() }}
COPY base/sudo.sh /usr/local/bin/sudo
RUN curl -L https://github.com/tianon/gosu/releases/download/1.16/gosu-amd64 -o /usr/local/bin/gosu \
&& chmod u+s /usr/local/bin/gosu \
&& chmod +x /usr/local/bin/gosu \
&& chmod +x /usr/local/bin/sudo
{% endblock %}
Warning
This is a privilege escalation by design. Keep it for local development, never ship an image built this way to production.
A conditional step¶
Build arguments are Twig variables, so a flag in compose.override.yaml is
enough to make a step optional:
services:
app-builder:
build:
args:
with_blackfire: 'true'
{% block builder %}
{{ parent() }}
{% if with_blackfire|default(false) %}
RUN curl -sSL https://packages.blackfire.io/binaries/blackfire/latest/blackfire_linux_amd64 -o /usr/local/bin/blackfire \
&& chmod +x /usr/local/bin/blackfire
{% endif %}
{% endblock %}
'true' is JSON-decoded into a real boolean, and |default(false) keeps the
build working for the containers where the argument is not set.
Custom nginx configuration (PhpMode::Fpm)¶
nginx.conf is itself a Twig template, so you can override one of its blocks
rather than maintain a copy. Put your version in the project, say
docker/nginx.conf.twig:
{% extends 'frontend/etc/nginx/nginx.conf.twig' %}
{% block server_locations %}
{{ parent() }}
location /downloads/ {
alias /var/www/var/downloads/;
}
{% endblock %}
Your project is not the build context, so expose it as an additional one in
compose.override.yaml:
services:
app:
build:
additional_contexts:
project: .
And render it over the shipped file:
# syntax=ghcr.io/castor-php/twig-dockerfile:0.1
{% extends 'Dockerfile' %}
{% block frontend %}
{{ parent() }}
{{ copy('@project/docker/nginx.conf.twig', '/etc/nginx/nginx.conf') }}
{% endblock %}
copy() renders the template with the build arguments and inlines the result,
so {{ php_version }} and friends work inside nginx.conf.twig too.
Custom Caddyfile (PhpMode::FrankenPhp)¶
The shipped Caddyfile.twig has no blocks: to change it, write your own and
render it over the original. The same additional context as above applies:
{% block frontend %}
{{ parent() }}
{{ copy('@project/docker/Caddyfile.twig', '/etc/frankenphp/Caddyfile') }}
{% endblock %}
Start from
the shipped one
if you use worker mode: the frankenphp_worker_* variables are yours to handle
in your copy.
A different base image¶
Drop {{ parent() }} and the block is replaced outright — useful when your
company ships its own hardened PHP image:
{% block php_base %}
FROM registry.example.com/php:{{ php_version }} AS php-base
ENV HOME=/home/app
ENV COMPOSER_MEMORY_LIMIT=-1
WORKDIR /var/www
{% endblock %}
Keep the stage name php-base and the WORKDIR: the other blocks build on
them.
Project files in the image¶
Sources are bind-mounted at /var/www, so you rarely need to copy them in
development. When you do — an entrypoint, a certificate — go through an
additional context:
COPY --from=project docker/entrypoint.sh /usr/local/bin/entrypoint