Blog

Infrastructure 11 min de lecture

Docker pour PHP en production : un guide de configuration pratique pour les applications métiers

Docker fait fonctionner une application PHP de la même manière sur un ordinateur portable, en test et en production, et rend les versions et les restaurations routinières au lieu de stressantes. Cela ajoute également des pièces mobiles, et une configuration négligente est pire qu'un simple serveur. Ce guide présente une base de référence prête pour la production avec une configuration réelle et indique les moments où vous ne devriez pas vous en soucier.

Illustration de couverture : Docker pour PHP en production : un guide de configuration pratique pour les applications métiers

En bref

  • Docker en vaut la peine lorsque vous souhaitez des environnements identiques, des restaurations rapides et une intégration facile. Pour un seul petit site de brochures sur l’hébergement partagé, cela est généralement inutile.
  • Créez une image immuable dans CI : construction en plusieurs étapes, dépendances de production uniquement, utilisateur non root, OPcache activé, erreurs enregistrées et jamais affichées.
  • Libérez par balise d'image (le hachage de validation Git), vérifiez l'état après le démarrage et revenez automatiquement à la balise précédente si la vérification échoue.
  • Les conteneurs sont jetables, les données ne le sont pas : conservez la base de données et les téléchargements dans des volumes ou des services gérés et sauvegardez-les en dehors Docker.
  • Les erreurs de production les plus courantes : secrets dans l'image, ports de base de données publiés, journaux illimités et limites php-fpm qui ne correspondent pas à la mémoire dont vous disposez.

Pour de nombreuses applications professionnelles, le coût réel de l’hébergement n’est pas celui du serveur mais celui des personnes. Une configuration qu'un seul développeur comprend, un déploiement effectué manuellement via FTP et un environnement de test qui se comporte différemment de la production sont tous coûteux à long terme. Les conteneurs résolvent ce problème en empaquetant l'application avec sa version exacte PHP et ses extensions. Nous gérons nos propres sites PHP et systèmes clients sur une pile basée sur Docker, et ce guide décrit la base de référence à partir de laquelle nous partons.

Quand Docker en vaut la peine et quand ce n'est pas le cas

  • Cela vaut le coup : plusieurs services (web, PHP, base de données, file d'attente), plus d'un développeur, un besoin de versions reproductibles et de restaurations rapides, ou un plan de déplacement entre les fournisseurs.
  • Pas nécessaire : un site au contenu simple qui change rarement sur un hébergement infogéré. La complexité accrue doit être récompensée.
  • Cela ne remplace pas de bonnes opérations : vous avez toujours besoin de correctifs, de sauvegardes et de surveillance. Consultez notre guide de sauvegarde et de surveillance.

L'architecture cible

Une configuration solide et simple pour une application PHP sur un seul serveur :

  • Proxy inverse et TLS en périphérie ( Nginx ou Caddy sur l'hôte, ou un CDN tel que Cloudflare, voir notre guide de configuration Cloudflare).
  • Conteneur Web ( Nginx) qui sert des fichiers statiques et transmet les requêtes PHP.
  • Conteneur PHP-FPM avec votre code, intégré à l'image.
  • Base de données dans son propre conteneur avec un volume persistant, ou mieux un service de base de données géré pour les données critiques.
  • Volumes uniquement pour les données qui doivent survivre : fichiers de base de données et téléchargements d'utilisateurs.

Un Dockerfile en plusieurs étapes

Une construction en plusieurs étapes installe les dépendances en une seule étape et copie uniquement le résultat dans une image d'exécution mince. L'image finale n'a pas Composer, de compilateurs ou d'outils de test, ce qui la rend plus petite et donne moins de possibilités aux attaquants :

# ---- stage 1: PHP dependencies ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader

# ---- stage 2: runtime ----
FROM php:8.3-fpm-alpine

RUN apk add --no-cache icu-libs libzip \
 && apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
 && docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
 && apk del .build

COPY docker/php.ini  /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf

WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .

USER www-data
EXPOSE 9000

Choisissez délibérément la version PHP et vérifiez qu'elle est toujours prise en charge. Notre guide de mise à niveau PHP 8 explique comment planifier cela. Épinglez les versions des images de base et mettez-les à jour selon un calendrier, car une image que vous ne reconstruisez jamais accumule des vulnérabilités connues. Conservez un .dockerignore qui exclut .git, .env, les tests et les données locales, afin que rien de sensible ne se retrouve dans l'image.

Configuration PHP de production et php-fpm

# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M

opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0   ; the code in an image never changes while it runs

# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no

Le réglage de opcache.validate_timestamps = 0 donne les meilleures performances pour une image immuable, mais cela ne fonctionne que si vous ne modifiez jamais de fichiers dans un conteneur en cours d'exécution : vous publiez une nouvelle image à la place. Dimensionnez le pool à partir de nombres réels : mesurez la mémoire moyenne d'un travailleur PHP sous charge, décidez de la quantité de mémoire PHP peut utiliser sur le serveur et divisez-la. Si un travailleur utilise 60 Mo et que vous pouvez donner 1 Go PHP, pm.max_children est d'environ 16. Le régler beaucoup plus haut ne fait que permuter le serveur ou tuer les processus du noyau lors d'un pic de trafic. Trop faible et file d'attente des demandes. Consultez le journal php-fpm pour les avertissements « max_children atteint ».

Un fichier Compose avec les bonnes valeurs par défaut

x-logging: &default-logging
  driver: json-file
  options: { max-size: "10m", max-file: "5" }     # logs cannot fill the disk

services:
  web:
    image: ghcr.io/your-org/app-web:${TAG}
    ports: ["127.0.0.1:8080:80"]                  # only the local reverse proxy can reach it
    depends_on: [php]
    restart: unless-stopped
    logging: *default-logging
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
      interval: 15s
      timeout: 3s
      retries: 3

  php:
    image: ghcr.io/your-org/app:${TAG}
    env_file: .env                                 # secrets stay on the server, not in the image
    restart: unless-stopped
    read_only: true
    tmpfs: [/tmp]
    mem_limit: 512m
    volumes: [uploads:/var/www/app/storage/uploads]
    logging: *default-logging

  db:
    image: mysql:8.4
    env_file: .env.db
    restart: unless-stopped
    volumes: [db_data:/var/lib/mysql]              # no "ports:" - the database is not public
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 15s
      retries: 5
    logging: *default-logging

volumes:
  uploads:
  db_data:

L'image web est une petite image Nginx avec votre dossier public/ et votre configuration de site copiés, construit dans le même pipeline que l'image PHP. Points clés : liez les ports publiés à 127.0.0.1, ne publiez jamais la base de données, définissez des politiques de redémarrage, limitez la mémoire afin qu'un conteneur ne puisse pas arrêter l'hôte et limitez la taille du journal. N'oubliez pas que les ports publiés par Docker peuvent contourner les règles de pare-feu de l'hôte, comme nous l'expliquons dans le guide Cloudflare. Ne montez jamais le socket Docker dans un conteneur d’application, car cela donne à ce conteneur le contrôle de l’hôte.

Versions et restaurations

La meilleure caractéristique d'une configuration basée sur une image est que chaque version est une balise et qu'une restauration consiste à "démarrer la balise précédente". Marquez les images avec le hachage de commit Git, ne comptez jamais sur latest et faites en sorte que le script de publication vérifie l'état :

#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"

export TAG
docker compose pull php web
docker compose up -d --no-deps php web

for i in $(seq 1 30); do
  if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
    echo "$TAG" > .current_tag
    echo "healthy: $TAG"; exit 0
  fi
  sleep 2
done

echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1

Les modifications de la base de données doivent être prudentes : écrivez les migrations pour que la version précédente du code fonctionne toujours avec le nouveau schéma (ajoutez d'abord des colonnes, supprimez-les dans une version ultérieure). C’est ce qui rend une restauration rapide sûre. Effectuez une sauvegarde de la base de données avant toute version modifiant le schéma.

Construction et expédition depuis CI

Créez l'image une fois dans CI et déployez exactement cet artefact. Un workflow GitHub Actions minimal :

name: build-and-release
on:
  push:
    branches: [main]
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/your-org/app:${{ github.sha }}
      - name: Release over SSH
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
          ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"

Exécutez vos tests et analyses statiques dans le même pipeline avant la création de l'image et utilisez une clé de déploiement qui ne peut exécuter que le script de publication. Ajoutez une analyse de vulnérabilité d’image si votre profil de risque l’exige.

Journaux, données et sécurité

  • Les journaux sont envoyés vers stdout et stderr, sont pivotés par Docker et, pour tout ce qui est important, expédiés vers un endroit central afin qu'ils survivent au conteneur.
  • Les données vivent en dehors du conteneur. Un volume n'est pas une sauvegarde. Utilisez l’approche de notre guide de sauvegarde : chiffrée, hors site, testée pour la restauration.
  • Les secrets ne sont jamais transformés en images. Transmettez-les au moment de l'exécution via des fichiers env ou un gestionnaire de secrets et gardez-les hors de Git.
  • Exécutez en tant qu'utilisateur non root, utilisez un système de fichiers en lecture seule lorsque cela est possible et supprimez ce dont vous n'avez pas besoin.
  • Gardez les images fraîches. Reconstruisez régulièrement pour récupérer les correctifs de sécurité dans l'image de base et PHP.
  • Appliquez les contrôles au niveau de l'application de notre liste de contrôle de sécurité PHP, car les conteneurs ne corrigent pas le code non sécurisé.

Une liste de contrôle de préparation à la production

  1. Image à plusieurs étapes avec dépendances de production uniquement, image de base épinglée, utilisateur non root.
  2. OPcache activé, erreurs enregistrées non affichées, taille php-fpm à partir de la mémoire mesurée.
  3. Composez un fichier avec des politiques de redémarrage, des contrôles de santé, des limites de mémoire, une rotation des journaux et aucun port de base de données public.
  4. Publications par balise de hachage de validation avec vérification de l'état et restauration automatiques.
  5. Sauvegardes et restauration testée de la base de données et des téléchargements.
  6. Surveillance et alertes sur la santé, le disque, les erreurs et les certificats.

Comment pouvons-nous vous aider

Nous conteneurisons les applications PHP, mettons en place des pipelines et des versions CI, et les connectons aux paiements, au CRM et à la messagerie. Consultez notre service d'intégration ou réservez un appel et nous examinerons votre processus d'hébergement et de publication actuel.

Questions fréquentes

Docker est-il bon pour exécuter PHP en production ?

Oui, quand il est bien utilisé. Il offre des environnements identiques, des versions reproductibles et des restaurations rapides. Il nécessite une configuration appropriée : images immuables, contrôles de santé, limites de ressources, rotation des journaux, gestion des secrets et sauvegardes des données en dehors du conteneur.

La base de données doit-elle fonctionner dans Docker en production ?

C'est possible pour les petits et moyens systèmes s'il utilise un volume persistant, n'est jamais exposé publiquement et a testé des sauvegardes. Pour les données critiques, de nombreuses équipes préfèrent un service de base de données géré, qui gère pour vous le basculement, les mises à jour et les sauvegardes.

Comment puis-je restaurer une version Docker ?

Marquez chaque image avec le hachage de validation Git et conservez la balise précédente. Pour revenir en arrière, démarrez les conteneurs avec la balise précédente. Écrivez les migrations de bases de données afin que l'ancien code fonctionne toujours avec le schéma le plus récent.

Combien de travailleurs php-fpm dois-je configurer ?

Mesurez la mémoire moyenne d'un travailleur sous charge, décidez de la quantité de mémoire PHP peut utiliser et divisez-la. Une valeur trop élevée entraîne un échange, tandis qu'une valeur trop faible entraîne la mise en file d'attente des requêtes. Regardez le journal php-fpm pour les avertissements max_children.

Quelle est la plus grosse erreur de sécurité Docker avec les applications PHP ?

Mettre des secrets dans l'image ou Git et publier des ports de base de données sur Internet. Évitez également d'exécuter en tant que root, de monter le socket Docker dans des conteneurs et de ne jamais mettre à jour l'image de base.

Appel découverte gratuit

Apportez une pile désordonnée ou une intégration bloquée - Integrono tracera le premier pas le plus intelligent.

Vous obtenez un avis senior sur les pièges budgétaires, les dépendances et s'il faut acheter, assembler ou construire - avant le début du travail payant. Le PDG reste dans la boucle.