Une image Docker PHP + Apache sur mesure pour la production

Créez l'image PHP + Apache qui vous ressemble. Elle sera donc parfaite puisque vous êtes un lecteur de ce blog et que vous êtes abonné.e aux nouveaux articles 💖

Une image Docker PHP + Apache sur mesure pour la production

C'est quoi une image Docker parfaite ?

C'est avant tout une image établie selon votre besoin et votre expérience. Vous avez identifié des patterns communs de configuration qui se répètent au fil de vos développements. Et comme vous êtes quelqu'un de bien, vous voulez donc les regrouper dans une couche commune à toutes vos apps pour en faciliter la maintenance.

À partir de ma propre expérience et du type d'applications que je développe la plupart du temps, une image PHP Apache parfaite selon moi c'est :

  • Composer installé par défaut (qui n'utilise pas de gestionnaire de dépendances de nos jours ?).
  • PHP et Apache configurés pour la production.
  • Des modules PHP récurrents activés par défaut.
  • De jolies page d'erreurs type 404 un peu plus silencieuses que celles par défaut d'Apache.
  • Idéalement des variantes déjà prêtes pour des applications qui tournent avec Symfony.

Créer son image parfaite

En premier lieu on va partir de l'image officielle de php, parce qu'on veut gagner du temps et ne pas réinventer la roue. Vous savez quoi ? L'image est même fournie avec un serveur Apache intégré. TOP.

🤓 « Pourquoi utiliser Apache plutôt que nginx ? »

Parce que cet article n'est pas neutre et qu'il faut trancher. Personnellement, je trouve la différence de performances négligeables entre les deux et Apache convient parfaitement pour la plupart des projets. C'est également plus serein pour les mises à jour de sécurité qui seront directement effectuées par la team PHP Docker. Il m'arrive par contre pour des projets un peu plus importants d'utiliser nginx. Encore une fois, cela reste vraiment une question de besoin, d'expérience et de goûts.

On va donc commencer notre Dockerfile avec la ligne suivante :

# Dockerfile
FROM php:8.2-apache

On build et on run tout ça pour vérifier que tout va bien :

docker build -t super/hero .
docker run -it -p 8080:80 super/hero

En vous rendant sur http://127.0.0.1:8080 vous devez tomber sur une belle page 403. Ça veut bien dire que tout fonctionne, il n'y a pas encore vos sources donc Apache vous répond gentiement !

Composer par défaut

Rien de plus simple, on installe la dernière version de composer et on rend la commande accessible partout dans le conteneur.

# Dockerfile
ENV COMPOSER_ALLOW_SUPERUSER=1
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN composer --version

Configuration d'Apache

Par défaut, l'image officielle est configurée pour avoir la racine du serveur Web dans /var/www/html. Trop long ! On va ignorer tout ça et accueillir notre futur code source dans un dossier /app directement à la racine du conteneur.

# conf/vhost.conf
<VirtualHost *:80>
	ServerAdmin [email protected]

	DocumentRoot /app

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
# conf/apache.conf
<Directory /app/>
	Options -Indexes +FollowSymLinks
	AllowOverride None
	Require all granted
</Directory>

ServerTokens Prod
ServerSignature Off
# Dockerfile
# ...
COPY conf/vhost.conf /etc/apache2/sites-available/000-default.conf
COPY conf/apache.conf /etc/apache2/conf-available/z-app.conf
RUN a2enconf z-app

On a changé ici la configuration du vhost de base d'Apache (000-default.conf), qui délivre maintenant le contenu du dossier /app. On configure aussi le dossier /app pour ne pas autoriser Apache de lister les fichiers d'un répertoire avec Options -Indexes.

Pour ma part, je n'aime pas configurer mes sites avec un .htaccess (pour des raisons de performance). C'est pourquoi j'ai placé la directive AllowOverride None. Comme l'image est destinée pour le staging / production, j'ai aussi supprimé les infos sur l'infrastructure utilisée avec les directives ServerTokens Prod et ServerSignature Off.

J'ai volontairement nommé la configuration z-app pour être sûr qu'elle soit chargée en dernier et qu'aucun autre module ne vienne surcharger les instructions.

Configuration de PHP

On va configurer PHP selon les besoins récurrents de nos futures images. Ne mettez pas de valeurs trop importantes ici, si un de vos conteneurs en particulier a besoin d'une configuration plus véloce, vous pourrez override cette configuration de base dans le Dockerfile propre à cette app.

; conf/php.ini
date.timezone = Europe/Paris

opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 128
opcache.revalidate_freq = 0
apc.enable_cli = On

upload_max_filesize = 16M
post_max_size = 16M

realpath_cache_size = 4096k
realpath_cache_ttl = 7200

display_errors = Off
display_startup_errors = Off
# Dockerfile
# ...
RUN apt-get update -qq && \
    apt-get install -qy \
    git \
    gnupg \
    unzip \
    zip 
RUN docker-php-ext-install -j$(nproc) opcache pdo_mysql
COPY conf/php.ini /usr/local/etc/php/conf.d/app.ini

Adaptez les modules par défaut qui vous semblent essentiels. Pour moi, la quasi totalité de mes projets utilisent PDO et MySQL. OPCache est bien sûr par défaut également.

Des jolies pages d'erreur par défaut

Qui ne s'est jamais retrouvé devant la page 404 par défaut d'Apache ? C'est l'assurance de perdre la crédibilité du ninja DevOp big data pro de baby-foot et autres buzzwords que vous êtes auprès de votre client. On va donc modifier la conf existante pour ajouter la gestion des pages d'erreurs :

# conf/apache.conf
# ...
<Directory /errors/>
	Options -Indexes
	AllowOverride None
	Require all granted
</Directory>

Alias /_errors/ /errors/
ErrorDocument 404 /_errors/404.html
ErrorDocument 403 /_errors/403.html
ErrorDocument 500 /_errors/500.html
# Dockerfile
# ...
COPY errors /errors

Pour ne pas causer de conflits avec les futures apps, j'ai placé le contenu des pages d'erreur dans un dossier à part (/errors à la racine du conteneur) et j'isole les requêtes HTTP spécifiques à ces pages via le chemin /_errors.

Vous pouvez récupérer directement le contenu du dossier errors dans le repo Github du tuto.

Ça donne quoi au final ?

Le Dockerfile a été optimisé pour réduire la taille des couches.

# Dockerfile
FROM php:8.2-apache

ENV COMPOSER_ALLOW_SUPERUSER=1

EXPOSE 80
WORKDIR /app

# git, unzip & zip are for composer
RUN apt-get update -qq && \
    apt-get install -qy \
    git \
    gnupg \
    unzip \
    zip && \
    curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \
    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# PHP Extensions
RUN docker-php-ext-install -j$(nproc) opcache pdo_mysql
COPY conf/php.ini /usr/local/etc/php/conf.d/app.ini

# Apache
COPY errors /errors
COPY conf/vhost.conf /etc/apache2/sites-available/000-default.conf
COPY conf/apache.conf /etc/apache2/conf-available/z-app.conf
COPY index.php /app/index.php

RUN a2enmod rewrite remoteip && \
    a2enconf z-app
# conf/apache.conf
<Directory /app/>
	Options -Indexes +FollowSymLinks
	AllowOverride None
	Require all granted

	SetEnvIf X_FORWARDED_PROTO https HTTPS=on
</Directory>

ServerTokens Prod
ServerSignature Off

<Directory /errors/>
	Options -Indexes
	AllowOverride None
	Require all granted
</Directory>

Alias /_errors/ /errors/
ErrorDocument 404 /_errors/404.html
ErrorDocument 403 /_errors/403.html
ErrorDocument 500 /_errors/500.html
# conf/vhost.conf
<VirtualHost *:80>
	ServerAdmin [email protected]

	DocumentRoot /app

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
; conf/php.ini
date.timezone = Europe/Paris

opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 128
opcache.revalidate_freq = 0
apc.enable_cli = On

upload_max_filesize = 16M
post_max_size = 16M

realpath_cache_size=4096k
realpath_cache_ttl=7200

display_errors = Off
display_startup_errors = Off

Curieux de voir le résultat ? J'ai mis en place rien que pour vous :

Ressources