Aller au contenu

Déploiement

Ce document décrit comment BCard est construit en image Docker, comment cette image est déployée en production et comment diagnostiquer un déploiement qui échoue. Le contenu est dérivé directement de Dockerfile, docker-compose.prod.yml, .github/workflows/deploy.yml, docker/entrypoint.prod.sh et docker/dotenv-prod.php.


Vue d'ensemble

Le déploiement en production repose sur trois éléments qui se succèdent :

  1. Dockerfile construit une image applicative autonome (code, dépendances Composer, extensions PHP, configuration Apache).
  2. Le workflow GitHub Actions .github/workflows/deploy.yml construit cette image, la pousse vers un registre Harbor, puis se connecte en SSH à l'hôte de production pour relancer la pile avec la nouvelle image.
  3. docker-compose.prod.yml, copié sur l'hôte de production par le workflow, décrit les services bcard et bcard-docs, ainsi que les variables d'environnement qu'ils attendent.

Image Docker

Dockerfile est construit en deux étapes (multi-stage build).

Étape vendor

Basée sur php:8.3-cli-bookworm. Elle installe les extensions PHP intl, sodium, mysqli, pdo_mysql, zip et gd via install-php-extensions, copie composer.json, composer.lock et symfony.lock, puis exécute :

composer install --no-dev --no-interaction --no-progress --no-scripts --prefer-dist --optimize-autoloader

Le code applicatif est ensuite copié et l'autoloader est régénéré en mode classmap autoritatif (composer dump-autoload --classmap-authoritative --no-dev). Cette étape sert uniquement à préparer un arbre de fichiers prêt à l'emploi ; elle n'est pas conservée dans l'image finale.

Étape app

Basée sur php:8.3-apache-bookworm. Elle réinstalle les mêmes extensions PHP que l'étape vendor (intl, sodium, mysqli, pdo_mysql, zip, gd), active les modules Apache rewrite et headers (a2enmod rewrite headers), puis copie depuis l'étape vendor l'arborescence /var/www déjà préparée.

Elle copie également :

  • docker/php/php-memory-limit.ini vers la configuration PHP,
  • docker/apache/apache.conf comme site Apache par défaut,
  • docker/apache/apache-php.conf, activé avec a2enconf apache-php,
  • docker/entrypoint.prod.sh, installé comme /usr/local/bin/bcard-entrypoint.

Les répertoires de téléversement (public/cv, public/profil_img, public/qrcodes, public/design, public/card, public/card_models, public/profil_user, public/brochure, public/uploads) sont créés puis attribués à www-data:www-data, de même que var et public. Les métadonnées Git éventuellement présentes dans l'arbre copié sont supprimées (find /var/www -name ".git*" -prune -exec rm -rf {} +).

L'image expose le port 80 et démarre avec bcard-entrypoint comme point d'entrée et apache2-foreground comme commande.

Configuration Apache et PHP

ENV APP_ENV=prod, ENV APP_DEBUG=0 et ENV COMPOSER_ALLOW_SUPERUSER=1 sont fixées dans l'image. APP_ENV et APP_DEBUG peuvent être surchargées par les variables d'environnement du conteneur (voir Pile de production).

Point d'entrée

docker/entrypoint.prod.sh s'exécute à chaque démarrage du conteneur, avant apache2-foreground :

  1. crée les répertoires var/cache, var/log et les répertoires de téléversement s'ils n'existent pas, puis réattribue var et public à www-data.
  2. attend que la base de données réponde à SELECT 1 (jusqu'à DB_WAIT_TRIES tentatives, 60 par défaut, avec une pause de 2 secondes entre chaque essai). L'erreur réelle est affichée dès la première tentative puis toutes les dix, pour ne pas masquer un problème de résolution DNS ou d'identifiants.
  3. vérifie la présence de la table doctrine_migration_versions. Si elle est absente et que la base ne contient aucune table, crée le schéma initial (doctrine:schema:create), initialise la table des métadonnées de migration puis marque toutes les migrations existantes comme déjà appliquées — pour ne pas rejouer, sur une base neuve, des migrations qui datent d'avant l'introduction du système de migration. Si des tables applicatives existent déjà, il initialise seulement les métadonnées de migration sans recréer le schéma.
  4. applique les migrations restantes (doctrine:migrations:migrate --allow-no-migration).
  5. vide et réchauffe le cache Symfony, puis réinstalle les assets publics (assets:install public --symlink).
  6. cède la main à la commande passée au conteneur (apache2-foreground) via exec "$@".

L'image ne contient pas de fichier .env : docker/dotenv-prod.php est copié dans l'image en tant que /var/www/.env.local.php. Ce fichier retourne un tableau vide ; sa seule fonction est d'exister, pour que Dotenv::bootEnv() (appelée par symfony/runtime à chaque point d'entrée, y compris bin/console) prenne la branche qui ne charge aucun fichier .env* et se contente des variables d'environnement déjà présentes dans le conteneur. Toute la configuration de production provient donc des variables passées par docker-compose.prod.yml, jamais d'un fichier .env embarqué dans l'image.


Pile de production

docker-compose.prod.yml déclare le service applicatif bcard, construit à partir de l'image ${DOCKER_IMAGE}:${IMAGE_TAG} (ces deux variables sont fournies par le workflow de déploiement, pas par l'utilisateur). Le service :

  • reçoit sa configuration entièrement par variables d'environnement (APP_SECRET, DATABASE_URL, les clés Stripe, reCAPTCHA, OAuth, JWT, etc.) — la majorité sont déclarées obligatoires (${VAR:?...} dans la syntaxe Compose : le démarrage échoue si la variable est absente). MAILER_DSN vaut smtp://mailer:1025 par défaut pour utiliser le service SMTP disponible sur le réseau infra-messaging ;
  • monte les répertoires de téléversement en volumes Docker nommés (un volume par type de fichier : CV, images de profil, QR codes, etc.) ainsi que ./jwt en lecture seule vers /var/www/config/jwt ;
  • est exposé via des labels Traefik (traefik.enable=true, routeur bcard, TLS via letsencrypt par défaut) et rattaché à trois réseaux externes, traefik-net, infra-data et infra-messaging, qui doivent déjà exister sur l'hôte.

Le détail de chaque variable d'environnement (rôle, caractère obligatoire, exemple neutre) est documenté dans Configuration, qui couvre à la fois le développement et la production — ce document ne le duplique pas.

Le même compose déclare aussi le service bcard-docs, construit par le workflow depuis docker/docs/Dockerfile et publié comme ${DOCS_IMAGE}:${IMAGE_TAG}. Il sert le portail technique statique via nginx derrière Traefik. Par défaut, l'URL publique est https://docs.bcard.169.58.138.150.nip.io ; elle peut être changée avec DOCS_HOST dans le fichier .env du répertoire de déploiement.

Variables non lues par le code applicatif

APP_BASE_URL, OBFUSCATE_ID_SECRET et MESSENGER_TRANSPORT_DSN sont toutes trois déclarées dans docker-compose.prod.yml, mais aucun code de src/ ni de config/ ne les lit. APP_BASE_URL et OBFUSCATE_ID_SECRET y utilisent la syntaxe obligatoire (${VAR:?...}) : leur absence fait échouer le démarrage du conteneur. MESSENGER_TRANSPORT_DSN y utilise une valeur par défaut (${MESSENGER_TRANSPORT_DSN:-doctrine://default}) : son absence ne bloque pas le démarrage. Voir Configuration pour le détail.


Chaîne d'intégration continue

Le workflow .github/workflows/deploy.yml (job unique deploy, sur ubuntu-latest) construit l'image et, selon l'événement déclencheur, la déploie sur l'hôte de production.

Déclencheurs

  • une poussée (push) sur la branche main ou sur la branche develop ;
  • une poussée d'un tag correspondant au motif v* ;
  • un déclenchement manuel (workflow_dispatch), avec un paramètre optionnel image_tag (voir Redéployer une image existante).

La construction de l'image (should_build) a lieu à chaque exécution du workflow, sauf quand workflow_dispatch est utilisé avec un image_tag déjà publié. Le redéploiement effectif sur l'hôte (should_deploy) n'a lieu, lui, que pour un déclenchement manuel ou pour une poussée sur develop — une poussée sur main ou un tag v* construit et publie l'image sans redéployer automatiquement.

Registre d'images

Les images sont poussées vers un registre Harbor auto-hébergé :

  • registre : binn.registry.169.58.138.150.nip.io ;
  • projet : binn ;
  • nom d'image : bcard.

Le registre est déclaré « insecure » côté runner GitHub Actions (/etc/docker/daemon.json et configuration Buildx), car il ne présente pas de certificat TLS reconnu depuis le runner. Chaque build est tagué avec le SHA court du commit (sha-<7 caractères>) et, selon le contexte, avec le nom de branche normalisé, latest (uniquement pour main) ou le nom du tag Git poussé.

Secrets requis

Le workflow référence les secrets GitHub suivants (cités par leur nom ; leurs valeurs ne sont pas documentées ici) :

  • secrets.REGISTRY_USERNAME et secrets.REGISTRY_PASSWORD — authentification auprès du registre Harbor, à la fois pour la publication de l'image depuis le runner et pour le docker login exécuté sur l'hôte de production avant le redémarrage de la pile ;
  • secrets.DEPLOY_HOST, secrets.DEPLOY_USER et secrets.DEPLOY_PATH — respectivement l'hôte SSH cible, l'utilisateur de connexion et le répertoire de déploiement distant. DEPLOY_PATH est optionnel : à défaut, le workflow utilise /srv/apps/bcard ;
  • secrets.DEPLOY_SSH_KEY — contenu de la clé privée SSH utilisée pour se connecter à l'hôte de production. Pour cette pile, il doit correspondre à la clé bcard_deploy autorisée sur le serveur ; le workflow l'écrit dans ~/.ssh/bcard_deploy avant les commandes ssh et scp.

Avant toute étape de déploiement, le workflow valide ces secrets : il retire les caractères de contrôle et les espaces superflus (un secret collé depuis un éditeur Windows arrive parfois avec un retour chariot, ce qui provoque un échec scp peu explicite), vérifie que DEPLOY_USER et DEPLOY_HOST ne sont pas vides et que DEPLOY_USER respecte un format d'identifiant Unix valide, puis échoue explicitement si l'une de ces conditions n'est pas remplie.

Le déploiement lui-même se fait par SSH : le workflow copie docker-compose.prod.yml et un fichier ci.env (contenant DOCKER_IMAGE et IMAGE_TAG) vers le répertoire de déploiement, puis, sur l'hôte, se connecte au registre Harbor et relance la pile avec docker compose --env-file .env --env-file ci.env -f docker-compose.prod.yml up -d --remove-orphans, suivi d'un docker image prune -f. Le fichier .env attendu sur l'hôte (distinct de ci.env, qui ne contient que l'image et le tag) doit déjà exister au préalable dans le répertoire de déploiement et porter le reste des variables listées dans Pile de production — le workflow échoue explicitement s'il est absent.


Redéployer une image existante

Un déclenchement manuel (workflow_dispatch) accepte un paramètre optionnel image_tag. Quand il est renseigné, le workflow saute entièrement l'étape de construction et de publication de l'image (should_build passe à false) et redéploie directement le tag Harbor indiqué sur l'hôte de production. C'est le mécanisme à utiliser pour revenir à une version antérieure déjà publiée, ou pour redéployer sans reconstruire après un incident sur l'hôte qui n'a pas touché l'image elle-même.


Remotes git

Le dépôt local peut être configuré avec plusieurs remotes git. Seul le remote nommé origin, pointant vers B-INNOV/BCard-web sur GitHub, correspond au dépôt sur lequel est configuré le workflow .github/workflows/deploy.yml. Pousser vers un remote git autre qu'origin ne déclenche donc aucun déploiement, même si la branche poussée s'appelle main ou develop — seule une poussée sur origin exécute le workflow décrit sur cette page.


Diagnostic

  • L'exécution du workflow échoue à l'étape « Validate deploy secrets » : un des secrets DEPLOY_HOST, DEPLOY_USER ou DEPLOY_SSH_KEY est absent ou mal formé côté GitHub. Le message d'erreur du job indique lequel.
  • Le déploiement échoue avec « Permission denied (publickey,password) » : la clé privée stockée dans secrets.DEPLOY_SSH_KEY ne correspond pas à la clé bcard_deploy autorisée dans ~/.ssh/authorized_keys de DEPLOY_USER sur l'hôte, ou l'utilisateur cible n'est pas celui qui possède cette autorisation.
  • Le conteneur bcard redémarre en boucle : consulter les logs du conteneur (docker logs bcard sur l'hôte). docker/entrypoint.prod.sh affiche la dernière erreur de connexion à la base de données dès la première tentative, puis toutes les dix tentatives, jusqu'à DB_WAIT_TRIES (60 par défaut) — un dépassement indique une base de données injoignable ou des identifiants incorrects dans DATABASE_URL.
  • Le déploiement échoue avec « Fichier .env manquant » côté hôte : le fichier .env attendu dans le répertoire de déploiement (/srv/apps/bcard par défaut) n'existe pas encore ; il doit être créé manuellement sur l'hôte avant le premier déploiement, avec l'ensemble des variables documentées dans Configuration.
  • Le conteneur démarre mais l'application renvoie une erreur 500 au boot : voir docker/dotenv-prod.php ci-dessus — si ce fichier n'est pas copié à /var/www/.env.local.php dans l'image, Dotenv::bootEnv() échoue au démarrage faute de trouver un fichier .env ou .env.local.php.