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 :
Dockerfileconstruit une image applicative autonome (code, dépendances Composer, extensions PHP, configuration Apache).- Le workflow GitHub Actions
.github/workflows/deploy.ymlconstruit 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. docker-compose.prod.yml, copié sur l'hôte de production par le workflow, décrit les servicesbcardetbcard-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.inivers la configuration PHP,docker/apache/apache.confcomme site Apache par défaut,docker/apache/apache-php.conf, activé aveca2enconf 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 :
- crée les répertoires
var/cache,var/loget les répertoires de téléversement s'ils n'existent pas, puis réattribuevaretpublicàwww-data. - attend que la base de données réponde à
SELECT 1(jusqu'àDB_WAIT_TRIEStentatives, 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. - 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. - applique les migrations restantes (
doctrine:migrations:migrate --allow-no-migration). - vide et réchauffe le cache Symfony, puis réinstalle les assets publics
(
assets:install public --symlink). - cède la main à la commande passée au conteneur (
apache2-foreground) viaexec "$@".
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_DSNvautsmtp://mailer:1025par défaut pour utiliser le service SMTP disponible sur le réseauinfra-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
./jwten lecture seule vers/var/www/config/jwt; - est exposé via des labels Traefik (
traefik.enable=true, routeurbcard, TLS vialetsencryptpar défaut) et rattaché à trois réseaux externes,traefik-net,infra-dataetinfra-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 branchemainou sur la branchedevelop; - une poussée d'un tag correspondant au motif
v*; - un déclenchement manuel (
workflow_dispatch), avec un paramètre optionnelimage_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_USERNAMEetsecrets.REGISTRY_PASSWORD— authentification auprès du registre Harbor, à la fois pour la publication de l'image depuis le runner et pour ledocker loginexécuté sur l'hôte de production avant le redémarrage de la pile ;secrets.DEPLOY_HOST,secrets.DEPLOY_USERetsecrets.DEPLOY_PATH— respectivement l'hôte SSH cible, l'utilisateur de connexion et le répertoire de déploiement distant.DEPLOY_PATHest 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_deployautorisée sur le serveur ; le workflow l'écrit dans~/.ssh/bcard_deployavant les commandessshetscp.
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_USERouDEPLOY_SSH_KEYest 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_KEYne correspond pas à la clébcard_deployautorisée dans~/.ssh/authorized_keysdeDEPLOY_USERsur l'hôte, ou l'utilisateur cible n'est pas celui qui possède cette autorisation. - Le conteneur
bcardredémarre en boucle : consulter les logs du conteneur (docker logs bcardsur l'hôte).docker/entrypoint.prod.shaffiche 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 dansDATABASE_URL. - Le déploiement échoue avec « Fichier .env manquant » côté hôte : le fichier
.envattendu dans le répertoire de déploiement (/srv/apps/bcardpar 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.phpci-dessus — si ce fichier n'est pas copié à/var/www/.env.local.phpdans l'image,Dotenv::bootEnv()échoue au démarrage faute de trouver un fichier.envou.env.local.php.