Base images for running Drupal sites in production: PHP with Apache, the PHP extensions Drupal needs, Composer, and drush cron built in.
Source and issues: https://github.com/keviny86/docker
| Tag | PHP | Platforms |
|---|---|---|
php-8.5-apache |
8.5 | amd64, arm64 |
php-8.4-apache |
8.4 | amd64, arm64 |
php-8.3-apache |
8.3 | amd64, arm64 |
php-8.2-apache |
8.2 | amd64, arm64 |
All tags are built from the same Dockerfile on Debian trixie; only the PHP version differs. Images are rebuilt every Saturday to pick up PHP and Debian security updates. Each build is also tagged with its date, for example php-8.5-apache-20261026; pin to a dated tag to roll back or to keep builds reproducible.
Using an 8.2–8.4 tag from before October 2026? See Upgrading from older builds.
- Apache serving
/var/www/html/web, withrewrite,expiresandheadersenabled - PHP extensions: gd (JPEG, WebP, FreeType), pdo_mysql, zip, OPcache, APCu, memcached
- Tools: Composer 2, git, unzip, MariaDB client (for
drush sql:*) - drush cron, scheduled by Supercronic
- The compilers PHP needs to build extensions, so sites can add their own
The image expects a Composer-based Drupal project (like drupal/recommended-project) in /var/www/html, with the docroot in web/ and drush installed by Composer. vendor/bin is on the PATH.
FROM keviny86/drupal:php-8.5-apache
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --optimize-autoloader
COPY config ./config
COPY web ./webservices:
drupal:
build: .
# Let a running cron finish when the container stops
stop_grace_period: 15m
environment:
DRUSH_OPTIONS_URI: "https://example.com"
DRUSH_CRON_SCHEDULE: "*/15 * * * *"
TZ: "Europe/Stockholm"
volumes:
- files:/var/www/html/web/sites/default/files
volumes:
files:The files directory must be writable by www-data.
| Variable | Default | Description |
|---|---|---|
DRUSH_CRON_SCHEDULE |
* * * * * |
When drush cron runs, in cron syntax. @hourly, @daily etc. work too. For seconds use 7 fields: */30 * * * * * * (second minute hour day month weekday year). |
DRUSH_CRON_TIMEOUT |
3600 |
Seconds before a cron run is stopped. 0 disables the timeout. |
TZ |
UTC | Timezone for the cron schedule. |
APACHE_MAX_REQUEST_WORKERS |
150 |
Maximum simultaneous requests. Each can use up to memory_limit (512M), so lower it to fit the container's memory. |
APACHE_DOCUMENT_ROOT |
/var/www/html/web |
Apache's document root. |
drush cron runs inside the container, next to Apache:
- It runs as
www-data, with the container's environment variables. - It is skipped while the site is in maintenance mode, for example during a deploy, and when maintenance mode can't be checked (such as while the database is starting).
- A new run never starts while the previous one is still running.
- Successful runs log nothing. Failures and warnings logged by Drupal during a run appear in
docker logs.
When running several replicas, cron runs in each of them; Drupal's cron lock makes sure only one run happens at a time.
A site image can add its own jobs as crontab files in /etc/supercronic.d/. They run as www-data next to drush cron, and their output appears in docker logs. Unlike drush cron, they also run in maintenance mode and have no timeout.
RUN echo '0 * * * * find /tmp -maxdepth 1 -name "*.tmp" -mmin +60 -delete' \
> /etc/supercronic.d/cleanup.crontabOn SIGTERM (docker stop, Kubernetes) the container:
- lets a running cron finish, and starts no new one
- stops Apache gracefully, waiting at most 30 seconds for requests in progress
Docker kills containers that haven't stopped after 10 seconds, so set stop_grace_period (Compose) or terminationGracePeriodSeconds (Kubernetes) to how long your cron may take. It's a maximum: an idle container stops in about a second.
The container also exits if Apache or the cron scheduler stops unexpectedly, so the restart policy can bring it back.
Based on php.ini-production, plus:
| Setting | Value |
|---|---|
memory_limit |
512M |
upload_max_filesize / post_max_size |
64M / 72M |
max_input_vars |
5000 |
apc.shm_size |
128M |
opcache.memory_consumption |
256 |
opcache.max_accelerated_files |
50000 |
opcache.validate_timestamps |
0 |
expose_php |
Off |
opcache.validate_timestamps=0 means PHP never rereads changed code: deploy new code by building a new image and restarting the container. drush cr does not clear Apache's OPcache.
To change a setting, add an ini file in your site image:
COPY php.ini /usr/local/etc/php/conf.d/zz-site.iniRun drush as www-data, so files it writes (translations, Twig cache) aren't owned by root:
docker exec -u www-data -e HOME=/tmp <container> drush statusThe MariaDB client in the image (11.8) requires TLS by default. If your database server doesn't offer TLS (MariaDB before 11.4), drush sql:* commands fail with SSL is required, but the server does not support it. Add this to the database connection in settings.php:
'pdo' => [\Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT => FALSE],On PHP 8.2 and 8.3, use \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT instead.
The compilers are kept in the image, so sites can install more extensions:
RUN apt-get update && \
apt-get install -y --no-install-recommends libicu-dev && \
docker-php-ext-install intl && \
rm -rf /var/lib/apt/lists/*RUN steps in images built from this one use bash -o pipefail, so a failure anywhere in a pipeline fails the build.
Every image is tested before it is published: the PHP setup, Apache configuration, cron and shutdown behaviour, a real Drupal install against MariaDB, and a scan for critical vulnerabilities that have a fix. To run the tests locally, from the source repository:
tests/lint.sh
docker build --build-arg PHP_VERSION=8.5 -t drupal-test drupal
tests/smoke.sh drupal-test
tests/cron.sh drupal-test
tests/drupal.sh drupal-test
tests/scan.sh drupal-testUntil October 2026 the 8.2–8.4 tags ran drush cron as root (through Debian's cron on 8.2 and 8.3, every 30 seconds on 8.4), and had no Debian trixie base, graceful shutdown or the settings above. When a site moves to a current build:
- Files owned by root: cron and drush now run as
www-data, so files the old root cron created can't be changed. Runfind web/sites/default/files ! -user www-data -exec chown www-data:www-data {} +in the container once, before running drush. - Debian's cron is gone: jobs a site image added to
/etc/crontabdon't run any more. Move them to/etc/supercronic.d/. - Cron schedule: the default is every minute; set
DRUSH_CRON_SCHEDULEto keep another schedule, such as 8.4's*/30 * * * * * *. - Shutdown: set
stop_grace_period, see Graceful shutdown. drush sql:*: with a database server without TLS, add the setting from Running drush.