Aller au contenu

Ouvrir Kirexo dans un Dev Container PHPStorm

Vous êtes développeur·se sur Kirexo, vous utilisez PHPStorm, et vous voulez un environnement de développement qui ne dépende plus de votre machine — où Claude Code peut tourner en mode bypassPermissions sans risquer d'accéder au reste de votre système. À la fin de ce tutoriel, vous aurez ouvert le projet dans un Dev Container PHPStorm, lancé castor cs:fix depuis l'intérieur, et vérifié que l'application répond bien sur http://localhost côté hôte.

Comptez quinze à vingt minutes au premier lancement, en grande partie occupées par le build de l'image FrankenPHP et l'application du pare-feu interne.

Prérequis

Sur votre machine :

  • PHPStorm 2024.1 ou plus récent (la fonctionnalité Dev Containers est intégrée depuis cette version).
  • git, docker, docker-compose — comme pour l'installation classique.
  • Le dépôt Kirexo cloné en local.
  • L'image PHP/FrankenPHP buildée au moins une fois (l'ouverture en mode Dev Container la rebuild si besoin).

Créer les fichiers sensibles ${HOME}/.claude avant le premier démarrage

compose.devcontainer.yaml monte en lecture seule trois fichiers de ${HOME}/.claude (.credentials.json, settings.json, CLAUDE.md) pour qu'un Claude compromis depuis l'intérieur ne puisse pas les réécrire — voir Durcissements de sécurité.

Si l'un de ces trois fichiers n'existe pas côté hôte au moment où le devcontainer démarre, Docker crée silencieusement un dossier vide à sa place et Claude Code part en erreur au boot. À lancer une fois avant votre premier Reopen in Container :

touch "${HOME}/.claude/.credentials.json" "${HOME}/.claude/settings.json" "${HOME}/.claude/CLAUDE.md"

Inoffensif si les fichiers existent déjà (touch met juste à jour leur mtime).

Ajuster les UID/GID du build si votre identité hôte n'est pas 1000

Au build, l'utilisateur app du conteneur est réaligné sur votre UID/GID hôte pour que les fichiers créés dans /app (vendor/, var/, composer.lock…) vous appartiennent côté hôte. Castor injecte ces valeurs automatiquement, mais PHPStorm/Gateway lance docker compose lui-même, hors Castor : il ne profite pas de cette injection.

Le projet versionne un défaut USER_ID=1000 / GROUP_ID=1000 dans .env (le seul dotenv que Docker Compose lit — voir l'encadré ci-dessous). Si votre UID et votre GID valent 1000, vous n'avez rien à faire, passez à l'étape 1.

Vérifiez votre identité :

id -u   # votre UID, ex. 1000
id -g   # votre GID, ex. 1001

Si l'une des deux valeurs diffère de 1000, exportez USER_ID/GROUP_ID dans l'environnement de la session avant de lancer l'IDE. Deux options :

  • Persistant (systemd user, recommandé) — créez ~/.config/environment.d/kirexo.conf :

    USER_ID=1000
    GROUP_ID=1001
    

    (remplacez par vos valeurs réelles). Les variables environment.d sont chargées à l'ouverture de session : déconnectez-vous puis reconnectez-vous pour qu'elles soient prises en compte par l'IDE lancé depuis votre environnement de bureau.

  • Ponctuel (shell) — si vous démarrez l'IDE depuis un terminal :

    export USER_ID=$(id -u) GROUP_ID=$(id -g)
    phpstorm .
    

Ne mettez pas ces variables dans .env.local

.env.local est lu par Symfony, pas par Docker Compose : Compose ne lit qu'un seul dotenv, .env. Y déclarer USER_ID/GROUP_ID n'a donc aucun effet sur le build du devcontainer. Pour surcharger le défaut 1000, passez impérativement par une vraie variable d'environnement (les deux options ci-dessus). Détail dans la Référence des variables d'environnement.

VS Code marche pareil

Le devcontainer de Kirexo est un .devcontainer/ standard, lu indifféremment par PHPStorm, VS Code et tout client compatible. Si vous travaillez sous VS Code, installez l'extension Dev Containers puis ouvrez la palette de commandes (F1) et choisissez Dev Containers: Reopen in Container. Le reste du tutoriel s'applique tel quel.

Le devcontainer.json ne déclare aucun bloc customizations.vscode (seulement customizations.jetbrains). À votre première ouverture, installez à la main les extensions VS Code dont vous avez besoin depuis le Marketplace — en particulier anthropic.claude-code si vous voulez piloter Claude Code depuis VS Code. Le Marketplace est déjà whitelisté par le pare-feu (marketplace.visualstudio.com). Le mode bypassPermissions de Claude Code, lui, est appliqué de façon identique sous PHPStorm et VS Code via .claude/settings.local.json créé par post-create.sh — pas via les settings IDE.

Étape 1 — Lancer PHPStorm en mode Remote Development

PHPStorm ouvre les Dev Containers depuis sa fenêtre de bienvenue Remote Development, pas depuis un projet déjà ouvert localement.

  1. Démarrez PHPStorm.
  2. Sur l'écran d'accueil, choisissez Remote Development dans le panneau de gauche.
  3. Dans la liste de droite, sélectionnez Dev Containers.
  4. Cliquez sur New Dev ContainerFrom Local Project.

Étape 2 — Sélectionner le projet et le devcontainer.json

PHPStorm vous demande deux choses :

  1. Le dossier du projet — pointez sur la racine du dépôt Kirexo cloné.
  2. Le fichier devcontainer.json — sélectionnez .devcontainer/devcontainer.json.

Le panneau de configuration s'ouvre avec le contenu du devcontainer.json détecté. Validez avec Build Container and Continue.

Étape 3 — Premier lancement

PHPStorm enchaîne plusieurs opérations qui peuvent prendre du temps au premier lancement :

Phase Ce qui se passe
Build de l'image Le stage frankenphp_dev du Dockerfile est construit (FrankenPHP + Xdebug + Chromium + outillage devcontainer : sudo, iptables, ipset, dnsmasq, jq, castor, glab, shellcheck).
Démarrage de la stack Les services compose.yaml + compose.override.yaml + compose.devcontainer.yaml sont empilés et démarrés (FrankenPHP, Postgres, Redis, RabbitMQ, Typesense, Gotenberg, Mailpit, mkdocs).
postCreateCommand Lance /app/.devcontainer/post-create.sh. Deux étapes : (1) crée .claude/settings.local.json (si absent) avec bypassPermissions — active le mode sans-confirmation pour Claude Code en terminal, sous PHPStorm et sous VS Code (mécanisme unique, indépendant des settings IDE). Cette étape est sautée si l'un des deux opt-out est actif : exporter KIREXO_CLAUDE_BYPASS=0 côté hôte avant l'ouverture (one-shot, à ré-exporter à chaque session) ou créer un fichier vide .devcontainer/no-claude-bypass dans le projet (persistant, gitignored). Si vous ne voulez pas du bypassPermissions, posez l'un de ces deux marqueurs avant votre premier Reopen in Container — sinon le settings.local.json est créé et il faudra le supprimer à la main. (2) amorce le projet via castor install (composer install + importmap:install + tailwind:build). Si la seconde étape échoue (réseau, contrainte composer), postCreate n'avorte pas : le devcontainer s'ouvre quand même, mais vendor/ peut être absent et l'application refusera de booter tant que vous n'aurez pas relancé castor install manuellement. Le script ne touche pas au fichier AGENTS.md à la racine : celui-ci est généré par le composer plugin symfony/ai-mate-composer-plugin.
postStartCommand Lance /app/.devcontainer/post-start.sh qui enchaîne deux étapes : sudo init-firewall.sh (pare-feu sortant — seuls les domaines whitelistés sont joignables depuis l'intérieur), puis le démarrage de FrankenPHP en arrière-plan (nohup frankenphp run … --watch, logs dans var/log/devcontainer/frankenphp.log — lisibles aussi bien depuis l'hôte que depuis l'intérieur du conteneur grâce au bind-mount sur /app). Vous n'avez rien à lancer manuellement pour servir l'application.
Backend PhpStorm PHPStorm installe son backend distant dans le conteneur, puis ouvre la fenêtre de l'IDE connectée à ce backend.

Healthcheck enrichi

Le service php expose en mode devcontainer un healthcheck qui valide aussi le pare-feu interne (présence de dnsmasq, de sa config générée, policy OUTPUT DROP toujours en place via firewall-healthcheck.sh, réponse de FrankenPHP sur :2019/metrics) — pas seulement FrankenPHP. start_period: 90s couvre le démarrage complet (pare-feu + supervisor) ; castor doctor côté hôte et le --wait de docker compose up peuvent donc attendre que tout soit opérationnel, pas juste que la page d'accueil réponde. Détail dans la Référence du Dev Container.

Le pare-feu effectue en fin d'exécution une série de vérifications automatiques :

  • Contrôle négatif (bloquant) : 203.0.113.1 (IP de la plage RFC 5737 TEST-NET-3, réservée à la documentation et garantie non routable) doit être inaccessible — preuve que la policy OUTPUT DROP est effective. On teste sur une IP brute plutôt que sur un domaine pour rester proche du modèle de menace (filtrage par socket, pas seulement par résolution DNS) et pour éviter qu'un domaine de test finisse par accident dans la whitelist. Si ce test échoue, le postStartCommand retourne une erreur.
  • Contrôles positifs (non bloquants) : gitlab.com, symfony.com, packagist.org, registry.npmjs.org doivent répondre. Un avertissement est émis pour chaque domaine injoignable sans pour autant échouer — utile quand une panne externe rend un service temporairement indisponible.
  • Résolution intra-stack (bloquante) : database, redis, rabbitmq, typesense, gotenberg, mailer doivent résoudre — un échec signale un DNAT cassé ou un service Compose à l'arrêt.

Si une vérification bloquante échoue, relancez tout le bloc (pare-feu + FrankenPHP) depuis un terminal interne PHPStorm avec /app/.devcontainer/post-start.sh, ou rechargez juste le pare-feu avec castor devcontainer:firewall-reload. Si vous voyez des avertissements sans erreur fatale, le pare-feu est actif mais une partie de la whitelist externe est injoignable — voir Debugger le pare-feu.

Logs FrankenPHP

FrankenPHP étant lancé en arrière-plan, sa sortie n'apparaît pas dans la fenêtre du terminal. Pour la consulter (utile en cas de page blanche ou d'erreur 500), depuis un terminal interne au conteneur ou directement depuis l'hôte (le fichier est bind-monté via /app) :

tail -f var/log/devcontainer/frankenphp.log

Étape 4 — Valider l'environnement

Une fois la fenêtre PHPStorm connectée au backend distant, ouvrez un terminal (View → Tool Windows → Terminal). Vous êtes maintenant à l'intérieur du conteneur php, avec /app comme dossier de travail.

Lancez le formateur PHP :

castor cs:fix

Castor détecte automatiquement qu'il tourne dans le conteneur (variable KIREXO_INSIDE_CONTAINER=1 injectée par compose.devcontainer.yaml) et exécute vendor/bin/php-cs-fixer fix directement, sans repasser par docker compose exec php …. Cf. la fonction is_inside_container() dans castor.php.

Vous pouvez vérifier au passage que vous êtes bien dans le conteneur :

echo $KIREXO_INSIDE_CONTAINER
# → 1
hostname
# → un identifiant de conteneur, pas le nom de votre machine

Tapez castor puis Tab pour voir l'autocomplétion s'activer sur les cibles : elle est préinstallée dans l'image du devcontainer, vous n'avez rien à configurer.

Étape 5 — Ouvrir l'application depuis votre navigateur hôte

La stack est démarrée dans le conteneur, mais les ports sont publiés sur votre hôte par Docker Compose. Depuis votre navigateur côté hôte (en dehors du conteneur), ouvrez :

Si la page d'accueil s'affiche, votre Dev Container est fonctionnel.

Vous pouvez aussi valider en une ligne depuis un terminal hôte :

curl -sI http://localhost | head -1

Vous devez obtenir une réponse HTTP/1.1 200 OK (ou un 302 si la home redirige). Si la commande renvoie Connection refused, FrankenPHP n'a pas démarré — consultez var/log/devcontainer/frankenphp.log (lisible directement depuis l'hôte, ou depuis l'intérieur du conteneur).

Ce qui change par rapport à l'installation classique

À l'intérieur du Dev Container, certaines cibles Castor ne fonctionnent pas : tout ce qui pilote Docker (castor docker:up, docker:down, docker:build, docker:logs, docker:sh, cert:trust, doctor, docs:build) suppose un socket Docker, volontairement absent du devcontainer pour préserver l'isolation. Ces cibles affichent un message d'erreur explicite — relancez-les depuis un terminal hôte (en dehors du devcontainer).

Toutes les autres cibles (castor cs:fix, phpstan, lint, test:unit, test:e2e, composer, console, ts:check, …) tournent normalement depuis l'intérieur.

Et ensuite ?

En cas de pépin

  • Le postStartCommand échoue avec « ERREUR : example.com est joignable » — le pare-feu n'a pas pu poser ses règles iptables. Vérifiez que la capability NET_ADMIN est bien présente sur le service php (cf. compose.devcontainer.yaml) et relancez castor devcontainer:firewall-reload (ou l'appel direct sudo /app/.devcontainer/init-firewall.sh).
  • Le postCreateCommand affiche « ⚠️ castor install a échoué » — l'amorçage du projet (composer + importmap + tailwind) a raté. Le devcontainer s'ouvre quand même mais vendor/ est probablement absent (Symfony refusera de booter, le MCP server symfony-ai-mate ne démarrera pas, Tailwind/AssetMapper renverra des 404). Le script écrit .devcontainer/.bootstrap.failed (JSON {status, exit_code, at}) pour tracer l'échec. La cible castor devcontainer:doctor consulte ce marqueur et le remonte en première ligne — un amorçage KO y apparaît avant tout autre check. Marche à suivre depuis un terminal interne du devcontainer :
    1. Vérifiez le réseau : curl -fsI https://packagist.org/ doit répondre 200 OK. Si le pare-feu bloque, voir Whitelister un domaine ou Debugger le pare-feu.
    2. Relancez l'amorçage via la cible dédiée : castor devcontainer:bootstrap. Elle rejoue castor install et bascule automatiquement le marqueur (.bootstrap.failed.bootstrap.ok si succès) — pas besoin de toucher aux fichiers à la main.
    3. En dernier recours, castor reinstall purge composer.lock + vendor/ puis résout depuis les contraintes courantes (attention, c'est destructif sur le lock). Relancer ensuite castor devcontainer:bootstrap pour mettre le marqueur à jour.
    4. Vérifiez avec castor devcontainer:doctor que la première ligne « Amorçage initial (post-create.sh) » est repassée au vert.
  • castor devcontainer:doctor rapporte « Amorçage initial (post-create.sh) ✗ KO » — le marqueur .bootstrap.failed existe, ou aucun marqueur n'a été écrit. Lancez castor devcontainer:bootstrap ; en cas d'échec, suivez les étapes du point précédent.
  • FrankenPHP ne répond plus (502 dans le navigateur, Connection refused sur http://localhost) — le supervisor a probablement bail-out après plusieurs crashs consécutifs, ou un processus a été tué à la main. Marche à suivre depuis un terminal interne du devcontainer :
    1. Inspectez les logs : tail -n 100 var/log/devcontainer/frankenphp.log (la commande marche depuis l'hôte comme depuis l'intérieur du conteneur — même fichier via bind-mount). Cherchez une ligne du type ${MAX_CRASHES} crashs consécutifs sans stabilisation (>${STABLE_AFTER}s) — abandon. — c'est le marqueur d'un bail-out. Les lignes précédentes pointent vers la cause (erreur dans Caddyfile, code PHP qui plante au boot, port :80 occupé).
    2. Une fois la cause corrigée, relancez le supervisor : castor devcontainer:frankenphp-restart. La cible est idempotente (tue d'abord toute instance résiduelle) puis redémarre FrankenPHP en arrière-plan avec les logs réinitialisés.
    3. Validez avec castor devcontainer:doctor (la ligne « FrankenPHP répond sur http://127.0.0.1 » doit repasser au vert) ou un curl -sI http://localhost côté hôte.
  • castor docker:up répond « cible … pilote Docker — elle ne peut pas tourner depuis l'intérieur du Dev Container » — comportement attendu. Lancez la cible depuis un terminal hôte.
  • Une commande échoue avec « connection refused » ou « could not resolve host » — c'est probablement le pare-feu qui bloque un domaine non whitelisté. Voir Whitelister un domaine ou Debugger le pare-feu.
  • Le build échoue avec groupmod: invalid group ID '' — le build a été lancé hors Castor (typiquement JetBrains Gateway) dans un environnement où USER_ID/GROUP_ID étaient interpolées à vide. Le défaut 1000:1000 versionné dans .env corrige normalement ce cas ; si l'erreur persiste, c'est que votre .env local ne contient pas la section ###> build Docker (dev uniquement) ### — récupérez-la depuis la version du dépôt (git checkout .env). Si votre UID/GID hôte n'est pas 1000, suivez en plus Ajuster USER_ID/GROUP_ID.
  • Les fichiers créés dans le conteneur (vendor/, var/…) appartiennent au mauvais propriétaire côté hôte — votre UID ou GID hôte n'est pas 1000 et le build est retombé sur le défaut. Exportez USER_ID/GROUP_ID puis rebuildez le conteneur — voir Ajuster USER_ID/GROUP_ID.
  • PHPStorm refuse d'ouvrir le devcontainer — vérifiez que vous êtes bien sur PHPStorm 2024.1 ou plus récent ; les versions antérieures n'ont pas le support Dev Containers.