# Coding using a devcontainer A series of 7 article(s), in reading order. # Développement PHP dans un devcontainer avec des outils de qualité de code préinstallés > Arrêtez de vous battre avec les problèmes de qualité de code. Mettez en place un environnement de développement PHP cohérent instantanément ! Ce guide pas à pas vous montre comment configurer un devcontainer VSCode avec des outils de qualité et d'analyse préinstallés. --- ![Développement PHP dans un devcontainer avec des outils de qualité de code préinstallés](/img/v2/devcontainer.webp) **TL;DR** Ce long article pas à pas construit un devcontainer VSCode pour PHP avec un ensemble complet d'outils de qualité de code préinstallés : PHP-CS-Fixer et PHPCBF/PHPCS pour le formatage automatique à la sauvegarde, SonarLint et Intelephense pour la détection de bugs en temps réel, et Rector pour la mise à niveau automatisée du code et les suggestions de refactoring — tout est configuré une fois dans `devcontainer.json` et `Dockerfile`, pour que chaque membre de l'équipe obtienne un environnement identique. Imaginons une des situations suivantes : 1. Vous travaillez à plusieurs sur le même projet PHP et vous remarquez qu'une personne ou une autre ne respecte pas vos critères de qualité concernant la mise en forme du code. Vous aimez utiliser quatre espaces pour l'indentation, vous voulez que l'accolade qui marque le début d'une fonction soit sur la ligne, vous ne voulez plus voir d'espaces inutiles en fin de ligne, ... et zut ! vous constatez que certains s'en fichent complètement. 2. Vous travaillez seul sur un projet et vous ne voulez pas perdre de temps à configurer votre VSCode. Vous souhaitez démarrer très vite avec beaucoup d'outils déjà installés dans VSCode, pour vous concentrer sur le code, pas sur l'éditeur. La solution ultime : utiliser un `devcontainer` dans VSCode. En utilisant un **devcontainer**, vous (et vos collègues) utiliserez un environnement préinstallé et tout le monde aura exactement le même. Vous gagnerez beaucoup de temps en évitant de configurer votre système, et vous pourrez coder tout de suite, épaulé par toute une série d'outils d'analyse de qualité. ## Ce qu'un devcontainer fait pour vous {#what-a-devcontainer-does-for-you} Voici un fichier PHP tel que je l'ai enregistré, avec mes pires habitudes bien visibles — indentation aléatoire, espaces en fin de ligne (affichés en points), accolades posées n'importe où : ![Index php avec du code mal écrit](./images/index_php_bad_formatting.webp) Maintenant le même fichier, après un simple `CTRL`+`S` dans le devcontainer. Je n'ai rien changé ; j'ai juste enregistré : ![Votre script a été correctement formaté cette fois](./images/index_php_correctly_formatted.webp) Rien n'a été installé sur ma machine pour obtenir ça — ni PHP, ni Composer, ni binaire de formatage. Et le collègue qui clone le repository obtient exactement le même comportement, sans qu'on lui demande de configurer quoi que ce soit. ## Pourquoi ça fonctionne {#why-it-works} * **L'image Docker embarque les outils.** PHP-CS-Fixer, PHPCS/PHPCBF et Composer sont téléchargés lors du build de l'image, donc ils existent dans le container et nulle part ailleurs. * **`devcontainer.json` embarque l'éditeur.** Il liste les extensions VSCode à installer et les réglages à appliquer — y compris « formate ce fichier à la sauvegarde, avec ce binaire, en utilisant ce fichier de configuration ». * **VSCode s'attache au container.** Votre éditeur continue de tourner sur votre machine, mais tout ce qu'il exécute se passe dans le container. C'est pour ça qu'un chemin comme `/usr/local/bin/php-cs-fixer.phar` est parfaitement valide même s'il n'existe pas sur votre disque. > **Vous voulez juste l'environnement prêt à l'emploi ?:** > > Alors sautez tout ce tutoriel et allez voir [Install a PHP Docker environment in a matter of seconds](/blog/php-devcontainer) : même résultat, trois commandes, aucune explication. ## Récupérer les fichiers {#get-the-files} Cet article est écrit sous forme de tutoriel pas à pas. Si vous ne voulez pas prendre le temps de créer les fichiers de configuration vous-même et préférez les télécharger directement, lancez les commandes ci-dessous dans une console Linux. Vous obtiendrez tous les fichiers et, chaque fois que le tutoriel ci-dessous vous demandera de créer un fichier, vous l'aurez déjà. ```bash $ mkdir /tmp/devcontainer_php && cd $_ curl -LOJ --silent https://github.com/cavo789/php_devcontainer/archive/refs/tags/1.0.0.tar.gz tar -xzvf php_devcontainer-1.0.0.tar.gz --strip-components 1 && rm -f php_devcontainer-1.0.0.tar.gz ``` > **Télécharger la toute dernière version:** > > Le repository php\_devcontainer va évoluer avec le temps. Si vous souhaitez télécharger la dernière version et non celle figée pour cet article, utilisez les commandes suivantes et non celles ci-dessus : > > ```bash > $ mkdir /tmp/devcontainer_php && cd $_ > > $ curl -LOJ --silent https://github.com/cavo789/php_devcontainer/archive/refs/heads/main.tar.gz > > $ tar -xzvf php_devcontainer-main.tar.gz --strip-components 1 && rm -f php_devcontainer-main.tar.gz > ``` ## 1. Créer le projet {#1-create-the-project} Démarrons tout de suite un nouveau projet en créant un répertoire temporaire avec `mkdir /tmp/devcontainer_php && cd $_`. Comme nous allons créer quelques fichiers, lancez `code .` pour démarrer Visual Studio Code (alias *VSCode*). Créez un nouveau fichier appelé `index.php` avec le code PHP ci-dessous : **index.php** ```php **Arrêtez de lire ici si ...:** > > ... vous êtes du genre à penser *Le script fonctionne, non ? Alors pourquoi tout ce bruit ?*, **éteignez votre ordinateur et promettez de ne plus jamais toucher une seule ligne de code**. Même si le code *fonctionne* ... il pue. Dans cet article, nous allons apprendre à configurer Visual Studio Code pour qu'il arrive avec des extensions préinstallées et configurées pour la version de PHP que vous ciblez (ce sera 8.2 mais vous pouvez changer ça très facilement). > **La technique que nous allons utiliser s'appelle devcontainer.:** > > **Un devcontainer est un environnement de développement isolé et préconfiguré qui tourne dans un container Docker, offrant les mêmes outils et réglages sur différentes machines.** ## 2. Installer d'abord l'extension VSCode ms-azuretools.vscode-docker {#2-install-first-the-ms-azuretoolsvscode-docker-vscode-extension} Dans VSCode, appuyez sur `CTRL`+`SHIFT`+`X` pour ouvrir le panneau `Extensions` et recherchez l'extension Docker de Microsoft appelée `ms-azuretools.vscode-docker` ; si vous ne l'avez pas encore, installez-la et activez-la. ## 3. Créer le fichier devcontainer.json {#3-create-the-devcontainerjson-file} L'étape suivante consiste à créer un dossier appelé `.devcontainer` et, dedans, un fichier appelé `devcontainer.json` avec le contenu ci-dessous. Dans la suite de cet article, nous reviendrons sur ce fichier. **.devcontainer/devcontainer.json** ```json { "name": "php_devcontainer", "build": { "dockerfile": "Dockerfile", "args": { "PHP_VERSION": "8.2", "COMPOSER_VERSION": "2.5.5", "PHPCSFIXER_INSTALL": "true", "PHPCSFIXER_VERSION": "3.46.0", "PHPCBF_INSTALL": "true", "PHPCBF_VERSION": "3.7.2" } }, "workspaceMount": "source=${localWorkspaceFolder},target=/var/www/html,type=bind", "workspaceFolder": "/var/www/html", "remoteUser": "docker", "customizations": { "vscode": { "settings": { "[php]": { "editor.formatOnSave": true, "editor.defaultFormatter": "junstyle.php-cs-fixer" }, "editor.codeActionsOnSave": { "source.fixAll": true }, "editor.formatOnSave": true, "editor.renderWhitespace": "all", "files.autoSave": "onFocusChange", "files.eol": "\n", "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "intelephense.environment.phpVersion": "8.2", "intelephense.telemetry.enabled": false, "php-cs-fixer.config": "/var/www/html/.config/.php-cs-fixer.php", "php-cs-fixer.executablePath": "/usr/local/bin/php-cs-fixer.phar", "php-cs-fixer.onsave": true, "php-cs-fixer.rules": "@PSR12", "php.validate.executablePath": "/usr/local/bin/php", "phpsab.executablePathCBF": "/usr/local/bin/phpcbf.phar", "phpsab.executablePathCS": "/usr/local/bin/phpcs.phar", "phpsab.fixerEnable": true, "phpsab.snifferShowSources": true, "phpsab.standard": "/var/www/html/.config/phpcs.xml", "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash" }, "extensions": [ "bmewburn.vscode-intelephense-client", "junstyle.php-cs-fixer", "ms-azuretools.vscode-docker", "ValeryanM.vscode-phpsab", "zobo.php-intellisense" ] } }, "postCreateCommand": "composer require rector/rector --dev" } ``` ## 4. Créer le fichier Dockerfile {#4-create-the-dockerfile-file} Continuons et créons un second fichier appelé `Dockerfile` avec ce contenu : **Dockerfile** ```docker ARG PHP_VERSION=8.2 ARG COMPOSER_HOME="/var/cache/composer" ARG COMPOSER_VERSION=2.5.5 ARG PHPCSFIXER_INSTALL=true ARG PHPCSFIXER_VERSION=3.46.0 ARG PHPCBF_INSTALL=true ARG PHPCBF_VERSION=3.7.2 ARG TIMEZONE="Europe/Brussels" ARG OS_USERID=1000 ARG OS_USERNAME="docker" FROM composer:${COMPOSER_VERSION} AS composer_base FROM php:${PHP_VERSION}-fpm ARG TIMEZONE ENV TZ=${TIMEZONE} ARG PHPCSFIXER_INSTALL ARG PHPCSFIXER_VERSION RUN set -e -x \ && if [ "$PHPCSFIXER_INSTALL" = "true" ] ; then \ printf "\e[0;105m%s\e[0;0m\n" "Install php-cs-fixer.phar ${PHPCSFIXER_VERSION}" \ && curl -L https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v${PHPCSFIXER_VERSION}/php-cs-fixer.phar -o /usr/local/bin/php-cs-fixer.phar \ && chmod +x /usr/local/bin/php-cs-fixer.phar ; \ fi ARG PHPCBF_INSTALL ARG PHPCBF_VERSION RUN set -e -x \ && if [ "$PHPCBF_INSTALL" = "true" ] ; then \ printf "\e[0;105m%s\e[0;0m\n" "Install phpcbf.phar ${PHPCBF_VERSION}" \ && curl -L https://github.com/squizlabs/PHP_CodeSniffer/releases/download/${PHPCBF_VERSION}/phpcbf.phar -o /usr/local/bin/phpcbf.phar \ && chmod +x /usr/local/bin/phpcbf.phar \ && curl -L https://github.com/squizlabs/PHP_CodeSniffer/releases/download/${PHPCBF_VERSION}/phpcs.phar -o /usr/local/bin/phpcs.phar \ && chmod +x /usr/local/bin/phpcs.phar ; \ fi ARG COMPOSER_HOME ENV COMPOSER_HOME=${COMPOSER_HOME} RUN set -e -x \ && mkdir -p "${COMPOSER_HOME}/cache/files" \ && mkdir -p "${COMPOSER_HOME}/cache/vcs" \ && chmod -R 777 "${COMPOSER_HOME}" COPY --from=composer_base /usr/bin/composer /usr/bin/composer ARG OS_USERID ARG OS_USERNAME RUN set -e -x \ && useradd --password '' -G www-data,root -u ${OS_USERID} -l -d "/home/${OS_USERNAME}" "${OS_USERNAME}" \ && mkdir -p "/home/${OS_USERNAME}" \ && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}" RUN set -e -x \ && printf "\e[0;105m%s\e[0;0m\n" "Install Linux binaries" \ && apt-get update -yqq \ && apt-get install -y --no-install-recommends curl libzip-dev zip unzip \ && docker-php-ext-install zip \ && docker-php-source delete \ && apt-get clean \ && rm -rf /tmp/* /var/list/apt/* WORKDIR /var/www/html ``` ## 5. Réouvrir dans le container {#5-reopen-in-the-container} Nous avons donc maintenant trois fichiers : un `index.php` dégueulasse, `.devcontainer/devcontainer.json` et `.devcontainer/Dockerfile`. Ce que nous allons faire maintenant, c'est *sauter* dans un container Docker. Regardez en bas à gauche de votre écran : ![Exécution dans WSL](./images/running_in_wsl.webp) Vous verrez quelque chose comme, dans mon cas, `WSL: Ubuntu-20.04`. *Cela peut être différent sur votre ordinateur selon votre système d'exploitation.* Ce que VSCode dit ici, c'est *je code sur ma machine*. Cliquez sur ce statut ou, autre possibilité, appuyez sur `CTRL`+`SHIFT`+`P` pour ouvrir la *Command Palette* puis cherchez `Reopen in Container` et appuyez sur `Enter`. ![Reopen in Container](./images/reopen_in_container.webp) Visual Studio Code va faire beaucoup de choses et, la première fois, cela peut prendre quelques minutes. ![Maintenant, vous êtes dans le container](./images/dev_container_vscode.webp) ## 6. Travailler dans le container {#6-working-in-the-container} Et voilà la magie : réouvrez le fichier `index.php` si mal formaté et enregistrez-le **sans aucune modification**. Appuyez juste sur `CTRL`+`S` et tadaaa 🎉🎉🎉 — vous obtenez le fichier reformaté montré au début de cet article. > **Comment est-ce possible ?:** > > C'est parce que nous avons appris à VSCode à utiliser un formateur spécifique pour nos fichiers PHP et que nous lui avons dit de formater le fichier à chaque sauvegarde. Et savez-vous où nous avons fait ça ? Dans nos fichiers `.devcontainer/devcontainer.json` et `.devcontainer/Dockerfile`, bien sûr ! > > **C'est pour ça que nous travaillons dans un devcontainer !** La configuration est faite une fois pour toutes, il n'y a plus qu'à la réutiliser, quel que soit le projet PHP. ## Les outils (prenez ceux dont vous avez besoin) {#the-tools-pick-the-ones-you-need} Les cinq sections ci-dessous sont indépendantes les unes des autres : chacune couvre un outil déjà installé dans le container, ce qu'il détecte, et où il a été déclaré. Lisez celles qui vous intéressent et sautez le reste — l'environnement fonctionne de toute façon avec tous activés. ### Le premier outil utilisé ici est PHP-CS-Fixer {#the-first-tool-weve-used-here-is-php-cs-fixer} Quand nous enregistrons un fichier dans VSCode, nous avons configuré un *formateur* qui va adapter la mise en forme du fichier selon les règles que nous avons définies. Par exemple, le nombre d'espaces pour l'indentation ou le fait que les accolades doivent être alignées (et bien d'autres choses). Cet outil est [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer). Mais voyons quand `PHP-CS-Fixer` a été installé... Réouvrez votre fichier `.devcontainer/devcontainer.json` et regardez les lignes surlignées ci-dessous : **.devcontainer/devcontainer.json** ```json { // ... "build": { "dockerfile": "Dockerfile" }, // ... "customizations": { "vscode": { "settings": { // highlight-next-line "[php]": { // highlight-next-line "editor.formatOnSave": true, // highlight-next-line "editor.defaultFormatter": "junstyle.php-cs-fixer" }, // ... // highlight-next-line "files.autoSave": "onFocusChange", // ... // highlight-next-line "php-cs-fixer.config": "/var/www/html/.config/.php-cs-fixer.php", // highlight-next-line "php-cs-fixer.executablePath": "/usr/local/bin/php-cs-fixer.phar", // highlight-next-line "php-cs-fixer.onsave": true, // highlight-next-line "php-cs-fixer.rules": "@PSR12", //... }, "extensions": [ // ... // highlight-next-line "junstyle.php-cs-fixer", ] } } } ``` Nous demandons donc à VSCode de lancer, pour les fichiers PHP, un `formatter` quand le fichier est enregistré, et l'outil à utiliser pour le formatage est `junstyle.php-cs-fixer`. Regardez directement à la fin, dans la section `extensions` : nous demandons à VSCode d'installer cette extension. Nous demandons aussi à VSCode d'enregistrer automatiquement le fichier dès que le focus change, par exemple si nous sélectionnons un autre fichier, si nous cliquons ailleurs (plus dans l'éditeur), ... Enfin, `junstyle.php-cs-fixer` demande quelques réglages à initialiser comme par exemple `php-cs-fixer.executablePath`. Là, vous pouvez vous dire : *Attendez une minute, je n'ai pas le fichier `/usr/local/bin/php-cs-fixer.phar` sur mon ordinateur.* Et vous avez probablement raison, mais réfléchissez : codez-vous *sur votre ordinateur* ou êtes-vous *dans un container Docker* ? En exécutant VSCode dans un container, vous n'êtes *plus* sur votre machine. Et maintenant, il faut regarder le troisième fichier que nous avons créé : `Dockerfile`. Réouvrez le fichier `.devcontainer/Dockerfile` et regardez les lignes surlignées ci-dessous : **.devcontainer/Dockerfile** ```docker # highlight-next-line ARG PHP_VERSION=8.2 # ... # highlight-next-line ARG PHPCSFIXER_INSTALL=true # highlight-next-line ARG PHPCSFIXER_VERSION=3.46.0 # ... # highlight-next-line FROM php:${PHP_VERSION}-fpm # ... # highlight-next-line ARG PHPCSFIXER_INSTALL # highlight-next-line ARG PHPCSFIXER_VERSION # highlight-next-line RUN set -e -x \ # highlight-next-line && if [ "$PHPCSFIXER_INSTALL" = "true" ] ; then \ # highlight-next-line printf "\e[0;105m%s\e[0;0m\n" "Install php-cs-fixer.phar ${PHPCSFIXER_VERSION}" \ # highlight-next-line && curl -L https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/download/v${PHPCSFIXER_VERSION}/php-cs-fixer.phar -o /usr/local/bin/php-cs-fixer.phar \ # highlight-next-line && chmod +x /usr/local/bin/php-cs-fixer.phar ; \ # highlight-next-line fi # ... ``` Ce fichier va demander à VSCode de télécharger une image Docker `php:8.2-fpm` et d'installer `PHP-CS-Fixer` (puisque `PHPCSFIXER_INSTALL` a été mis à `true`). La version `3.46.0` de l'exécutable `PHP-CS-Fixer` sera téléchargée et stockée, dans l'image Docker, sous `usr/local/bin/php-cs-fixer.phar`. C'est pour ça que, dans notre `devcontainer.json`, nous pouvons écrire `"php-cs-fixer.executablePath": "/usr/local/bin/php-cs-fixer.phar",` : parce que nous avons déjà téléchargé et installé l'exécutable. > **Besoin de vérifier ?:** > > Toujours dans VSCode, ouvrez un terminal en appuyant sur `CTRL`+`´` et, dans le prompt, tapez `ls -l /usr/local/bin`. Comme vous le voyez, `php-cs-fixer.phar` est bien là. *Il a été téléchargé et installé par VSCode quand vous avez sauté dans le container.* > > ![Le binaire php-cs-fixer.phar est bien là](./images/php-cs-fixer-phar.webp) #### PHP-CS-FIXER - Fichier de configuration {#php-cs-fixer---configuration-file} Le fichier `.devcontainer/devcontainer.json` contient aussi cette ligne : `"php-cs-fixer.config": "/var/www/html/.config/.php-cs-fixer.php"`. Nous pouvons donc avoir notre propre fichier de configuration pour `PHP-CS-Fixer` (avec nos propres règles) et ce fichier doit être créé dans un dossier appelé `.config` et nommé `.php-cs-fixer.php`. Essayons, et cette fois nous allons nous assurer que tous nos fichiers PHP auront un bloc d'en-tête avec notre copyright, nos informations de licence ou autre. Dans VSCode, créez le dossier `.config` et dedans le fichier `.php-cs-fixer.php` avec ce contenu : **.config/.php-cs-fixer.php** ```php in('.') ->exclude([ '.config', '.devcontainer', '.git', 'node_modules', 'vendor' ]); $header = file_get_contents(__DIR__ . '/licenseHeader.txt'); $config = new PhpCsFixer\Config(); return $config->setRules( [ '@PSR12' => true, 'header_comment' => [ 'header' => rtrim($header, "\r\n"), 'location' => 'after_declare_strict', 'comment_type' => 'PHPDoc', ], ] ) ->setCacheFile('/tmp/.php-cs-fixer.cache') ->setIndent(' ') ->setLineEnding("\n") ->setFinder($finder); ``` Créez aussi un second fichier appelé `licenseHeader.txt` avec le contenu que vous voulez, par exemple : **.config/licenseHeader.txt** ```txt This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ``` Maintenant, réouvrez votre fichier `index.php` et appuyez simplement sur `CTRL`+`S` pour l'enregistrer à nouveau et tadaaa... ![Un bloc de licence est automatiquement ajouté par php-cs-fixer](./images/license.gif) Comme vous le voyez, lors du formatage du fichier, `PHP-CS-Fixer` va désormais aussi injecter notre en-tête. > **Beaucoup d'options sont expliquées ici:** > > Rendez-vous sur ou pour en savoir plus sur les options du fichier de configuration de php-cs-fixer. #### PHP-CS-FIXER - Corriger tous les fichiers d'un coup {#php-cs-fixer---fix-all-files-at-once} Un pas de plus : est-il possible de lancer `PHP-CS-Fixer` non seulement sur le fichier en cours d'édition dans VSCode mais sur tous les fichiers de votre repository ? **Oui, bien sûr.** Dans VSCode, appuyez sur `CTRL`+`´` (ou cliquez sur le menu `View` puis `Terminal`) pour ouvrir un terminal. Assurez-vous d'être dans le dossier `/var/www/html` (lancez `cd /var/www/html` si besoin) et exécutez la commande suivante : ```bash $ /usr/local/bin/php-cs-fixer.phar fix --config /var/www/html/.config/.php-cs-fixer.php . ``` Cela va lancer `PHP-CS-Fixer` sur tout votre code (`.`). Si vous voulez exclure certains dossiers, ouvrez le fichier `.config/.php-cs-fixer.php` et mettez à jour le tableau `exclude`. ### Le deuxième jeu d'outils, ce sont PHPCBF et PHPCS {#the-second-set-of-tools-is-phpcbf-and-phpcs} Notre `Dockerfile` a été configuré pour télécharger et installer les outils [PHP\_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) appelés `phpcbf` et `phpcs`. `phpcbf` est un outil de `Code beautifier` : tout comme `PHP-CS-Fixer`, `phpcbf` va apporter quelques modifications mineures à vos fichiers selon les règles activées. Par exemple, mettez à jour votre script `index.php` avec ce contenu : **index.php** ```php **PHP-CS-Fixer sera appelé quand vous enregistrerez le fichier:** > > Vous avez remarqué ? Vous avez copié/collé le script ci-dessous où il n'y avait pas d'espaces avant et après le mot-clé `else`. Mais dès que vous enregistrez le fichier, boum, le fichier est refactoré par `PHP-CS-Fixer` et, entre autres, des espaces ont été ajoutés avant et après `else` mais pas pour `if`. C'est parce que, en réalité, il n'y a pas UN OUTIL MAGIQUE pour tout. Parfois `PHP-CS-Fixer` ne peut pas faire certaines modifications alors que `PHPCBF` oui, ou l'inverse. Comme pour `PHP-CS-Fixer`, nous avons déjà installé `PHPCBF` et `PHPCS` dans notre container. Voyez votre fichier `.devcontainer/devcontainer.json` : **.devcontainer/devcontainer.json** ```json { // .. "customizations": { "vscode": { "settings": { // ... // highlight-next-line "phpsab.executablePathCBF": "/usr/local/bin/phpcbf.phar", // highlight-next-line "phpsab.executablePathCS": "/usr/local/bin/phpcs.phar", // highlight-next-line "phpsab.fixerEnable": true, // highlight-next-line "phpsab.snifferShowSources": true, // highlight-next-line "phpsab.standard": "/var/www/html/.config/phpcs.xml", // ... }, "extensions": [ // ... // highlight-next-line "ValeryanM.vscode-phpsab", // ... ] } } } ``` Et les deux exécutables `/usr/local/bin/phpcbf.phar` et `/usr/local/bin/phpcs.phar` ont été téléchargés grâce à votre `Dockerfile`. Jetez-y un œil si besoin. #### PHPCS et PHPCBF - Fichier de configuration {#phpcs-and-phpcbf---configuration-file} Dans VSCode, créez dans le dossier `.config` un fichier appelé `phpcs.xml` avec ce contenu : **.config/phpcs.xml** ```xml ``` Maintenant que nous avons défini notre standard de codage (`PSR12` ici), affichez à nouveau le script `index.php` : ![PHPCS dans VSCode](./images/vscode_phpcs.webp) Ouvrez un terminal en appuyant sur `CTRL`+`´` (ou en cliquant sur le menu `View` puis `Terminal`) et lancez cette commande : ```bash $ /usr/local/bin/phpcs.phar --standard=/var/www/html/.config/phpcs.xml /var/www/html/index.php ``` Et vous obtiendrez cette sortie : ```bash FILE: /var/www/html/index.php ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ FOUND 1 ERROR AND 1 WARNING AFFECTING 2 LINES ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 1 | WARNING | [ ] A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects, or it should execute logic with side effects, but should not do | | both. The first symbol is defined on line 18 and the first side effect is on line 29. (PSR1.Files.SideEffects.FoundWithSymbols) 20 | ERROR | [x] Expected 1 space(s) after IF keyword; 0 found (Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ``` Il y a un avertissement et une erreur concernant une *violation de convention de codage* et le plus sympa, c'est que **phpcbf peut en corriger certaines**. Maintenant, lancez presque la même commande mais plus avec `phpcs` (pour détecter les violations), avec `phpcbf` (pour les corriger — celles qui peuvent l'être automatiquement). ```bash $ /usr/local/bin/phpcbf.phar --standard=/var/www/html/.config/phpcs.xml /var/www/html/index.php ``` Vous obtiendrez ceci : ```bash PHPCBF RESULT SUMMARY ---------------------------------------------------------------------- FILE FIXED REMAINING ---------------------------------------------------------------------- /var/www/html/index.php 1 1 ---------------------------------------------------------------------- A TOTAL OF 1 ERROR WERE FIXED IN 1 FILE ---------------------------------------------------------------------- ``` Et effectivement, si vous regardez votre script PHP, il y a maintenant un espace après le mot-clé `if`. #### PHPCS et PHPCBF - Tous les fichiers d'un coup {#phpcs-and-phpcbf---all-files-at-once} Vous avez probablement déjà compris la syntaxe. Utilisez simplement le `.` pour scanner tout le code : 1. Pour corriger les violations, lancez `/usr/local/bin/phpcbf.phar --standard=/var/www/html/.config/phpcs.xml .` 2. Pour scanner les violations restantes, lancez `/usr/local/bin/phpcs.phar --standard=/var/www/html/.config/phpcs.xml .`. ### Le troisième outil est SonarLint {#the-third-tool-is-sonarlint} Ici aussi, SonarLint était déjà installé dans notre container Docker grâce à notre fichier `.devcontainer/devcontainer.json` : **.devcontainer/devcontainer.json** ```json { // ... "customizations": { "vscode": { "extensions": [ // ... // highlight-next-line "sonarsource.sonarlint-vscode", // ... ] } } } ``` > **Ce n'est qu'une extension VSCode:** > > SonarLint n'a pas besoin d'outil externe ; c'est une extension VSCode autonome, donc rien à prévoir dans notre `Dockerfile`. Réouvrez donc le fichier `index.php` que vous avez, et faites attention à la ligne `echo sayHello()` : **index.php** ```php **SonarLint évite les bugs !:** > > Contrairement aux outils vus plus haut, il ne s'agit plus de formatage, **mais d'alerter le développeur sur des bugs potentiels et des erreurs fatales pendant qu'il écrit son code.** ### Le quatrième outil est Intelephense {#the-fourth-tool-is-intelephense} À côté de `SonarLint`, il y a aussi `Intelephense` qui va lui aussi détecter un certain nombre de bugs potentiels. Dans l'exemple précédent, quand le prototype était `function sayHello($firstname)`, `Intelephense` alertait aussi le développeur sur le paramètre manquant. **index.php** ```php **Nous avons besoin de plusieurs outils:** > > Cet exemple illustre bien le point : à ce jour, en février 2024, il faut encore jongler avec plusieurs extensions et outils pour obtenir un code propre et sans bug. ### Le meilleur pour la fin, le cinquième outil est Rector {#the-best-for-last-the-fifth-tool-is-rector} Rector est une application formidable pour scanner et mettre à niveau automatiquement votre code vers une version donnée de PHP, et cela signifie aussi inspecter votre façon de coder. Peut-être utilisez-vous encore une manière de programmer digne de PHP 5.4 mais indigne d'un développeur moderne à jour sur les dernières évolutions. > **Rector est mon coach personnel:** > > J'ADORE vraiment Rector puisqu'il m'aide à apprendre de nouvelles fonctionnalités de PHP. Quand je le lance sur n'importe lequel de mes projets, je vois où je peux améliorer mon code en refactorant certaines parties et faire mieux. > > **J'ADORE VRAIMENT RECTOR.** Cette fois, il va falloir installer Rector pour notre projet et pour cela, nous allons suivre la documentation officielle : mais ... c'est déjà fait dans notre fichier `.devcontainer/devcontainer.json`. Si vous le réouvrez, regardez le nœud `postCreateCommand` : **.devcontainer/devcontainer.json** ```json { // ... "customizations": { // ... }, // highlight-next-line "postCreateCommand": "composer require rector/rector --dev" } ``` Rector sera ajouté au fichier `composer.json` (et le fichier sera créé s'il n'existe pas encore). > **`Composer` a été installé dans notre `Dockerfile`:** > > Si vous pensez *Oui, mais je n'ai pas installé composer...*, eh bien c'est faux. Nous l'avons installé dans notre container. Revoyez votre `.devcontainer/Dockerfile` si besoin. #### Rector - fichier de configuration {#rector---configuration-file} > **Sautez ce chapitre si vous avez déjà téléchargé les fichiers de configuration:** La deuxième étape du guide d'installation nous demande de lancer la commande ci-dessous. À l'invite, répondez `yes` pour créer votre fichier de configuration `rector.php`. ```bash $ vendor/bin/rector ``` Enfin, ouvrez le fichier de configuration `rector.php` et mettez-le à jour comme ceci : **rector.php** ```php withSkip( [ '.config', '.devcontainer', '.git', 'node_modules','vendor' ] ) ->withPhpSets(php82: true) ->withPreparedSets(codeQuality: true, deadCode: true, typeDeclarations: true); ``` > **Dites à Rector quelle version de PHP vous utilisez:** > > Rector va vous suggérer des changements mais pour cela, il doit savoir quelle version de PHP vous utilisez. En effet, Rector ne suggérera pas une syntaxe PHP 8.2 par exemple si vous tournez encore sur une version plus ancienne (voir [PHP Version Features](https://getrector.com/documentation/php-version-features)). > **De mon côté, je préfère mettre tous les fichiers de configuration dans un dossier .config:** > > Vous avez remarqué que Rector a créé son fichier de configuration à la racine de votre projet. Je préfère le déplacer dans le dossier `.config`. #### Lancer Rector {#run-rector} Pour illustrer ce que Rector peut faire, éditez le `index.php` avec ce contenu, une fois de plus : **index.php** ```php ). Rector suggère de supprimer complètement l'expression `if .. else ...` et d'utiliser une [expression ternaire](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#simplifyifelsetoternaryrector). Et aussi, de retourner directement la valeur au lieu d'utiliser une variable `$text` inutile. Et c'est correct, nous pouvons écrire `return $firstname == "" ? "Hello World!" : "Hello " . $firstname;` pour obtenir exactement la même chose. Et si vous pensez que c'est fini, vous vous trompez. Rector regarde maintenant le paramètre de notre fonction et comprend que c'est une chaîne, donc il va suggérer d'[ajouter un type hint](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#strictstringparamconcatrector) et, aussi, d'[ajouter un type de retour](https://github.com/rectorphp/rector/blob/main/docs/rector_rules_overview.md#returntypefromstrictscalarreturnexprrector) pour la même raison. Puisque nous avons analysé les suggestions de Rector et que nous sommes d'accord, relancez la même commande mais, cette fois, sans le flag `--dry-run`. ```bash $ vendor/bin/rector process index.php --config .config/rector.php ``` Rector a mis à jour notre fichier `index.php` ! **Maintenant, notre fonction tient sur une seule ligne et non plus sept. Belle amélioration !** Notre nouveau code, amélioré, est maintenant : **index.php** ```php . > **Le programmeur a le dernier mot, mais pour combien de temps encore ?:** > > Nous pouvons faire une amélioration de plus en extrayant le préfixe `Hello`. Rector ne l'a pas encore vu, mais pour combien de temps ? > > **index.php** > > ```php > > function sayHello(string $firstname = ""): string > { > return "Hello " . ($firstname == "" ? "World!" : $firstname); > } > > echo sayHello(); > ``` #### Rector - Tous les fichiers d'un coup {#rector---all-files-at-once} Retournez dans votre terminal et lancez la commande ci-dessous pour scanner tout votre code. ```bash $ vendor/bin/rector process . --dry-run --config .config/rector.php ``` ##### Dry-run signifie qu'aucune modification n'est faite, juste affichée {#dry-run-means-no-changes-are-made-just-displayed} Quand vous lancez Rector avec `vendor/bin/rector process . --dry-run --config .config/rector.php`, aucun fichier ne sera réécrit. Rector se contentera de suggérer des changements. Si vous autorisez Rector à modifier vos fichiers, lancez plutôt `vendor/bin/rector process . --config .config/rector.php` mais, là, vous devez être sûr que c'est OK. > **Versionner son code avec par exemple GitHub:** > > Si vous utilisez un système de versioning, vous pouvez pousser votre code actuel sur GitHub, créer une nouvelle branch comme par exemple `refactoring` puis lancer Rector sans risque sur votre disque. Si quelque chose casse, vous pouvez toujours récupérer vos sources d'avant les modifications. ### Et on peut ajouter d'autres outils {#and-we-can-add-more-tools} Il existe encore plusieurs outils d'analyse de code pour PHP : * [phpstan](https://phpstan.org/), * [psalm](https://psalm.dev/), * [PHP Copy/Paste Detector](https://github.com/sebastianbergmann/phpcpd), * [PHP Magic Number Detector](https://github.com/povils/phpmnd), * ... Vous pouvez certainement aussi les ajouter à votre container. ### Extensions {#extensions} Certaines extensions seront installées automatiquement : * [bmewburn.vscode-intelephense-client](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client) - PHP code intelligence for Visual Studio Code, * [junstyle.php-cs-fixer](https://marketplace.visualstudio.com/items?itemName=junstyle.php-cs-fixer) - PHP CS Fixer extension for VS Code, php formatter, php code beautify tool, format html, * [ms-azuretools.vscode-docker](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker) - Makes it easy to create, manage, and debug containerized applications, * [sonarsource.sonarlint-vscode](https://marketplace.visualstudio.com/items?itemName=sonarsource.sonarlint-vscode) - Linter to detect & fix coding issues locally in JS/TS, Python, PHP, Java, C, C++, C#, Go, IaC. Use with SonarQube & SonarCloud for optimal team performance., * [streetsidesoftware.code-spell-checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Spelling checker for source code, * [usernamehw.errorlens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) - Improve highlighting of errors, warnings and other language diagnostics, * [ValeryanM.vscode-phpsab](https://marketplace.visualstudio.com/items?itemName=ValeryanM.vscode-phpsab) - PHP Sniffer & Beautifier for Visual Studio Code and * [zobo.php-intellisense](https://marketplace.visualstudio.com/items?itemName=zobo.php-intellisense) - Advanced Autocompletion and Refactoring support for PHP ## Conclusion {#conclusion} Souvenez-vous du collègue du début de cet article, celui qui n'indente pas comme vous. Il n'y a plus rien à lui demander. Les règles ne sont plus dans une page de wiki ou dans un commentaire de code review, elles sont dans deux fichiers versionnés — `devcontainer.json` et `Dockerfile` — et elles s'appliquent dès l'ouverture du projet, sur chaque machine, à l'identique. C'est vraiment là le changement : l'environnement n'est plus quelque chose que chaque développeur construit, mais quelque chose que le repository fournit. Si vous préférez lancer ces outils à la demande plutôt que de les intégrer dans une image, voyez [jakzal/phpqa](/blog/php-jakzal-phpqa). Et pour être sûr qu'aucun de ces outils de développement ne se retrouve dans votre image de production, lisez [One Docker Image for Production and Devcontainers - The Clean Way](/blog/docker-prod-devcontainer). --- # Installer un environnement PHP Docker en quelques secondes > Mettez en place rapidement un environnement PHP Docker Dev Container pour VSCode. Composer et des outils d'analyse statique préinstallés comme PHP-CS-Fixer et Rector, immédiatement disponibles. --- ![Installer un environnement PHP Docker en quelques secondes](/img/v2/devcontainer.webp) **TL;DR** Voici la version courte, façon démarrage rapide, de l'article complet sur le devcontainer : téléchargez le squelette `php_devcontainer` avec `curl`, ouvrez le dossier dans VSCode et cliquez sur « Reopen in Container » pour obtenir un environnement PHP prêt à l'emploi avec Composer, PHP-CS-Fixer, PHPCS/PHPCBF, SonarLint et Rector préinstallés — aussi bien pour un nouveau projet que pour une base de code existante. Cet article est la version très courte et directe de [PHP development in a devcontainer with preinstalled code quality tools](/blog/vscode-devcontainer). Si vous voulez juste suivre quelques étapes et obtenir votre environnement PHP, cet article est pour vous. **En suivant les étapes décrites dans cet article, vous obtiendrez** 1. Un environnement PHP Docker pour coder avec vscode, 2. Le gestionnaire de packages PHP `composer` installé, 3. Quelques outils d'analyse statique comme `php-cs-fixer`, `phpcs`, `phpcbf`, `sonarlint` et `Rector` installés 4. Un environnement devcontainer prêt à l'emploi. Suivez le guide... > **Le but de cet article est de créer un environnement de développement (aka devcontainer):** > > Consultez mes autres articles sur [Docker](/blog/tags/docker) pour voir comment dockeriser l'application et la faire tourner avec [Apache](/blog/tags/apache) et un service de base de données comme [PostgreSQL](/blog/tags/postgresql). ## Ce que l'environnement vous apporte {#what-the-environment-gives-you} Voici PHP-CS-Fixer en train de reformater un fichier, dans le container, quelques minutes après le téléchargement — rien d'installé sur ma machine hôte, pas de PHP, pas de Composer, aucune extension à configurer : ![PHP-CS-Fixer](./images/php-cs-fixer.webp) C'est pareil pour [PHP\_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) (`phpcbf.phar`), [Rector](https://github.com/rectorphp/rector) et SonarLint : ils sont déjà dans l'image, déjà configurés via le dossier `.config` livré avec le squelette. Vous trouverez les commandes exactes dans la section *Vous êtes prêt à utiliser les outils* ci-dessous. Deux façons d'y arriver, selon que vous partez de zéro ou que vous ajoutez ceci à une base de code existante. ## Vous n'avez rien et souhaitez créer un projet entièrement nouveau {#you-have-nothing-and-wish-to-create-a-fully-new-project} ### 1. Installer le squelette php\_devcontainer {#1-install-the-php\_devcontainer-skeleton} Pour cet article, je vais créer un dossier temporaire dans `/tmp/devcontainer_php`. N'hésitez pas à adapter le chemin, par exemple `~/my_project`, selon vos besoins. ```bash $ mkdir /tmp/devcontainer_php && cd $_ $ curl -LOJ --silent https://github.com/cavo789/php_devcontainer/archive/refs/heads/main.tar.gz $ tar -xzvf php_devcontainer-main.tar.gz --strip-components 1 && rm -f php_devcontainer-main.tar.gz ``` > **Télécharger une version spécifique:** > > Si vous ne voulez pas télécharger la dernière version mais une version précise, indiquez le tag comme ceci : `curl -LOJ --silent https://github.com/cavo789/php_devcontainer/archive/refs/tags/1.0.0.tar.gz`. Vous avez donc téléchargé le code de mon repo dans votre dossier. ### 2. Ouvrir VSCode {#2-open-vscode} Démarrez VSCode sur votre machine : ```bash $ cd /tmp/devcontainer_php $ code . ``` ### 3. Passer dans le Dev Container {#3-switch-to-the-dev-container} VSCode va automatiquement proposer d'ouvrir un Dev Container. Cliquez sur le bouton `Reopen in Container`. ![VSCode propose automatiquement d'ouvrir un Dev Container](./images/vscode_starting.webp) Si vous n'avez pas ce popup, appuyez simplement sur `CTRL`+`SHIFT`+`P` pour ouvrir la **Command Palette**, cherchez `Dev Containers: Rebuild and Reopen in Dev Container` et cliquez dessus. VSCode va maintenant prendre un peu de temps pour construire l'image Docker puis démarrer un container Docker avant que vous puissiez travailler. Cela peut prendre quelques minutes selon la vitesse de votre machine. ### Vous êtes prêt à utiliser les outils {#you-are-ready-to-use-tools} Appuyez sur `CTRL`+`ù` pour ouvrir le panneau **Terminal** et tapez-y `/usr/local/bin/php-cs-fixer.phar fix --config /var/www/html/.config/.php-cs-fixer.php index.php` pour lancer [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) — c'est la commande qui a produit la capture d'écran en haut de cet article. Vous pouvez aussi lancer `/usr/local/bin/phpcbf.phar --standard=/var/www/html/.config/phpcs.xml /var/www/html/index.php` pour reformater votre code avec PHP\_CodeSniffer. Vous pouvez également lancer Rector avec `vendor/bin/rector process index.php --config .config/rector.php`. Comme vous le voyez, en trois actions seulement, vous avez téléchargé et installé un environnement PHP fonctionnel avec Docker et VSCode. ## Vous avez déjà un projet existant {#you-already-have-an-existing-project} Dans un cas réel, vous avez déjà un projet PHP. Ouvrez un terminal et placez-vous dans le dossier de votre base de code. Pour cet article, je vais télécharger une très vieille base de code Laravel d'apprentissage réalisée il y a des années : ```bash $ cd /tmp $ git clone https://github.com/cavo789/laravel_todos.git ``` Mon projet sera donc dans le dossier `/tmp/laravel_todos`. Dans le dossier de mon projet, je vais maintenant lancer : ```bash $ cd /tmp/laravel_todos $ curl -LOJ --silent https://github.com/cavo789/php_devcontainer/archive/refs/tags/1.0.0.tar.gz $ tar -xzvf php_devcontainer-1.0.0.tar.gz --strip-components 1 && rm -f php_devcontainer-1.0.0.tar.gz ``` Cela va télécharger les fichiers du devcontainer PHP dans mon projet. Je lance `code .` pour ouvrir VSCode et, comme avant, VSCode va proposer d'ouvrir le dossier dans un Dev Container. Allons-y. Et voilà, j'ai dockerisé mon environnement de développement PHP. Dans mon cas, j'ai écrit `Laravel Todos` en 2018, avec PHP 7.1.3 et Laravel 5.x ; une éternité, donc. Et comme au chapitre précédent, je peux lancer les commandes ci-dessous pour corriger les problèmes de style et traiter tous les fichiers de mon repo : ```bash $ /usr/local/bin/php-cs-fixer.phar fix --config /var/www/html/.config/.php-cs-fixer.php . $ /usr/local/bin/phpcbf.phar --standard=/var/www/html/.config/phpcs.xml . ``` Et je **pourrai** lancer Rector pour refactorer toute ma base de code : ```bash $ composer require rector/rector --dev $ vendor/bin/rector process . --config .config/rector.php ``` mais dans mon cas, pas tout de suite : mon Laravel Todos est ancien et je dois d'abord modifier mon fichier `composer.json` (mettre à jour les versions de PHP et de Laravel). ## Conclusion {#conclusion} Un `curl`, un `tar` et un clic sur `Reopen in Container` : c'est toute la distance entre un dossier vide et un environnement PHP où les outils de qualité sont déjà là, dans la même version pour tous ceux qui clonent le repository. Rien n'a été installé sur votre machine, et supprimer le dossier supprime l'environnement. Même recette, autres stacks : [Docker - Python devcontainer](/blog/docker-python-devcontainer). Et quand vient le moment de livrer, [One Docker Image for Production and Devcontainers - The Clean Way](/blog/docker-prod-devcontainer) montre comment garder tout cet outillage hors de votre image de production. --- # Docker - Python devcontainer > Découvrez comment mettre en place rapidement un devcontainer VSCode pour développer en Python avec Docker. Récupérez le Dockerfile complet, le compose.yaml et les configurations .docker.env. --- ![Docker - Python devcontainer](/img/v2/devcontainer.webp) **TL;DR** Cet article construit un devcontainer Python réutilisable pour VSCode : un `Dockerfile`, un `compose.yaml`, un `.docker.env` (pour les réglages propres à chaque projet, comme la version de Python) et un `makefile` qui enveloppe les commandes courantes (`make up`, `make bash`), puis un `.devcontainer/devcontainer.json` préconfiguré avec l'extension Python et les settings nécessaires. Résultat : `make devcontainer` ouvre VSCode qui tourne entièrement dans le container, avec les fichiers synchronisés avec l'host. Comme vous le savez, VSCode est un superbe éditeur qui vous permet de programmer dans probablement n'importe quel langage. Un éditeur, pas un IDE, parce que VSCode est au fond un Notepad dans sa version ultime. Si vous voulez programmer en Python, vous aurez besoin d'installer quelques extensions dans VSCode pour être vraiment à l'aise : coloration syntaxique, navigation dans le code, refactoring (renommer une variable ou une classe), etc. Il existe des éditeurs « prêts à l'emploi » comme PyCharm mais 1. ils sont payants et 2. ils sont spécifiques (vous ne pourrez pas programmer en PHP avec PyCharm ; ni même travailler facilement avec des fichiers HTML/CSS). Dans ce nouvel article, nous allons voir comment obtenir un environnement VSCode immédiatement prêt à l'emploi pour coder en Python. Et comme c'est VSCode, c'est 1. gratuit, 2. polyvalent et 3. terriblement puissant. ## Voyons-le tourner {#lets-see-it-running} Voici ce que vous obtenez, avant même d'avoir créé un seul fichier : lancez `make up` dans votre console et vous verrez cet écran : ![Makefile](./images/make.webp) Comme vous le voyez, nous avons tout un tas de commandes, comme `make up` pour démarrer notre container Docker. Essayons, et aïe, il nous manque un fichier appelé `/src/requirements.txt`. ![requirements.txt is missing](./images/requirements_txt.webp) ### src/requirements.txt {#srcrequirementstxt} Créez un fichier vide appelé `src/requirements.txt`. Le fichier peut rester vide (vous y ajouterez vos propres dépendances plus tard). En relançant `make up`, ça y est ! Cette fois nous pouvons construire nos images et créer notre container : ![Docker up](./images/docker-up.webp) Cette fois, si besoin, nous pouvons entrer dans notre container en lançant `make bash` et, par exemple, vérifier ce que contient le dossier courant et quelle version de Python a été installée : ![Inside the container](./images/container-python.webp) > **Note:** > > Comme vous le voyez, c'est Python 3.13 qui est utilisé. Pourquoi cette version précise ? Retournez dans votre fichier `.docker.env` et regardez la variable `DOCKER_PYTHON_VERSION`. Si vous en voulez une autre, mettez simplement à jour le fichier `.docker.env` et relancez `make up`. ## Créons notre premier script {#lets-start-creating-our-first-script} Si vous êtes toujours dans votre container, tapez `exit` afin que votre prompt soit celui de votre machine (votre *host*), et non plus celui du container en cours d'exécution. Créez votre premier script : un nouveau fichier dans le dossier `src`, appelé `hello.py`. Soyez créatif et tapez un simple `print` : ```python print("I'm your Python code") ``` ![Hello](./images/hello.webp) Pour pouvoir exécuter le code, relancez `make bash` (pour entrer dans le container) puis lancez `python hello.py` ![Run hello](./images/run-hello.webp) > **Information:** > > Ça fonctionne parce que, dans le container, le répertoire de travail est `/app/src`. Si ça n'avait pas été le cas, il aurait fallu écrire par exemple `python /app/src/hello.py`, c'est-à-dire le chemin absolu vers le script. Comme vous pouvez le constater, les fichiers de votre machine sont synchronisés avec votre host. Si VSCode est toujours ouvert, vous pouvez modifier votre script, par exemple : ```python print("Hey! It's synchronized; cool!") ``` ![It's synchronized](./images/it-is-synchronized.webp) Les fichiers derrière tout ça — `Dockerfile`, `compose.yaml`, `.docker.env` et le `makefile` — sont présentés juste après. ## Créons les fichiers de notre environnement Docker {#lets-create-the-files-for-our-docker-environment} Dans cet article, nous allons créer le devcontainer, c'est-à-dire l'environnement de développement basé sur Docker que vous venez de voir tourner. La création du devcontainer prendra quelques minutes, mais il s'agit juste de copier/coller depuis cet article vers votre système. Une fois les fichiers créés, vous pourrez réutiliser le devcontainer pour tous vos projets Python. *Deux prolongements naturels : ajouter les outils de [Python - Code Quality tools](/blog/python-qa) à l'image pour que chaque projet en hérite, et lire [One Docker Image for Production and Devcontainers - The Clean Way](/blog/docker-prod-devcontainer) pour garder cet outillage en dehors de l'image que vous déployez.* Comme toujours, nous allons créer un nouveau dossier et y créer des fichiers. Lancez `mkdir /tmp/python && cd $_` pour créer ce dossier et vous y placer. Démarrez VSCode depuis là, c'est-à-dire lancez `code .` dans la console quand vous êtes dans le dossier. Cela ouvrira VSCode et vous pourrez créer autant de fichiers que vous voulez. ### Dockerfile {#dockerfile} Le premier fichier à créer servira à construire notre image Docker. Créez un fichier appelé `Dockerfile` avec le contenu suivant : **Dockerfile** ```docker # syntax=docker/dockerfile:1 # cspell:ignore PYTHONDONTWRITEBYTECODE,PYTHONUNBUFFERED,HISTFILE # cspell:ignore addgroup,adduser,keyscan,hadolint,gecos,endregion # Those variables are initialized in the .docker.env file ARG DOCKER_APP_HOME="/app" ARG DOCKER_CONTAINER_NAME="python" ARG DOCKER_OS_GROUPID=1000 ARG DOCKER_OS_USERID=1000 ARG DOCKER_OS_USERNAME="python" ARG DOCKER_PYTHON_VERSION=3.10-slim # region - Our Python base image. We'll install Linux and Python dependencies here # and do some other configuration work FROM python:${DOCKER_PYTHON_VERSION} AS base # Prevents Python from writing pyc files. ENV PYTHONDONTWRITEBYTECODE=1 # Keeps Python from buffering stdout and stderr to avoid situations where # the application crashes without emitting any logs due to buffering. ENV PYTHONUNBUFFERED=1 ARG DOCKER_APP_HOME WORKDIR "${DOCKER_APP_HOME}/src" # hadolint ignore=DL3008 RUN --mount=type=cache,target=/var/cache/apk,rw \ set -e -x \ && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries..." \ && apt-get update -yqq \ && apt-get install -y --no-install-recommends bash git openssh-client tree \ && apt-get clean \ && rm -rf /tmp/* /var/lib/apt/lists/* # Install Python dependencies # # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them into # into this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=src/requirements.txt,target=requirements.txt \ printf "\e[0;105m%s\e[0;0m\n" "Install Python dependencies" \ && python -m pip install --no-cache-dir -r requirements.txt # Keep the container running ENTRYPOINT ["tail", "-f", "/dev/null"] # endregion # region - Define our development image FROM base AS development ENV SHELL /bin/bash # Our user will be part of the root group since we're building the development image ARG DOCKER_OS_GROUPID ARG DOCKER_OS_USERID ARG DOCKER_OS_USERNAME RUN set -e -x \ && mkdir -p "/home/.vscode-server/bin" \ && mkdir -p "/home/.vscode-server/extensions" \ && mkdir -p "/home/.vscode-server/extensionsCache" \ && printf "\e[0;105m%s\e[0;0m\n" "Create our ${DOCKER_OS_USERNAME} application user" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/bin" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/extensions" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/extensionsCache" \ # Create the application user home directory && mkdir -p "/home/${DOCKER_OS_USERNAME}" \ # Create our application user group && addgroup "${DOCKER_OS_USERNAME}" --gid "${DOCKER_OS_GROUPID}" \ # Create our application user && adduser \ --system \ --disabled-password \ --gecos "" \ --home "/home/${DOCKER_OS_USERNAME}" \ --uid "${DOCKER_OS_USERID}" \ "${DOCKER_OS_USERNAME}" \ # And, finally, set the correct permissions to the home folder of our user && chown -R "${DOCKER_OS_USERNAME}:${DOCKER_OS_USERNAME}" "/home/${DOCKER_OS_USERNAME}" USER "${DOCKER_OS_USERNAME}" ARG DOCKER_CONTAINER_NAME RUN /bin/bash -c "echo \"PS1='\n\e[0;33m🐳 ${DOCKER_CONTAINER_NAME} \e[0;32mDEV\e[0m - \e[0;36m$(whoami)\e[0m \w # '\" >> /home/${DOCKER_OS_USERNAME}/.bashrc" # Save the bash history in file /home/${OS_USERNAME}/commandhistory/.bash_history # Like this we'll be able to map that folder using a volume in our # compose.yaml file and then make the history persistent. RUN set -e -x \ && SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/home/${DOCKER_OS_USERNAME}/commandhistory/.bash_history" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/commandhistory" \ && touch "/home/${DOCKER_OS_USERNAME}/commandhistory/.bash_history" \ && echo "${SNIPPET}" >> "/home/${DOCKER_OS_USERNAME}/.bashrc" \ && echo "${SNIPPET}" >> "/home/${DOCKER_OS_USERNAME}/.bashrc" # endregion ``` ### compose.yaml {#composeyaml} À côté du `Dockerfile`, nous allons créer notre `compose.yaml`. Créez ce fichier avec le contenu suivant : **compose.yaml** ```yaml name: app_python services: app_python: build: context: . target: development args: # Version of Python to use - DOCKER_PYTHON_VERSION=${DOCKER_PYTHON_VERSION} # Application root directory in the container (PHP or NGINX) (--app-home) - DOCKER_APP_HOME=${DOCKER_APP_HOME:-/app} # Name of the container to show in the bash prompt - DOCKER_CONTAINER_NAME=${DOCKER_CONTAINER_NAME:-python} # Set OS groupid in your Docker Linux containers (1000 = root) (--os-groupid) - DOCKER_OS_GROUPID=${DOCKER_OS_GROUPID:-1000} # Set OS userid in your Docker Linux containers (1000 = root) (--os-userid) - DOCKER_OS_USERID=${DOCKER_OS_USERID:-1000} # Set OS username in your Docker Linux containers (--os-username) - DOCKER_OS_USERNAME=${DOCKER_OS_USERNAME:-python} user: ${DOCKER_OS_USERID:-1000}:${DOCKER_OS_GROUPID:-1000} env_file: - .docker.env container_name: app_python volumes: # Our codebase on our host - .:${DOCKER_APP_HOME} # Keep installed VSCode extensions in a volume to avoid to reinstall them - vscode-extensions:/home/${DOCKER_OS_USERNAME}/.vscode-server/extensions # Remember the bash history - bashhistory:/home/${DOCKER_OS_USERNAME}/commandhistory volumes: # Use a Docker volume self-managed volume to store vscode's cache vscode-extensions: # Use a Docker volume self-managed volume to store bash history bashhistory: ``` ### .docker.env {#dockerenv} Le troisième fichier à créer s'appellera `.docker.env` ; nous y initialiserons quelques valeurs. Créez ce fichier avec le contenu ci-dessous : **.docker.env** ```env # Application root directory in the container (PHP or NGINX) (--app-home) DOCKER_APP_HOME=/app # Name of the container to show in the bash prompt DOCKER_CONTAINER_NAME=app # Set OS groupid in your Docker Linux containers (1000 = root) (--os-groupid) DOCKER_OS_GROUPID=1000 # Set OS userid in your Docker Linux containers (1000 = root) (--os-userid) DOCKER_OS_USERID=1000 # Set OS username in your Docker Linux containers (--os-username) DOCKER_OS_USERNAME="python" # Version of Python to use DOCKER_PYTHON_VERSION=3.13-slim ``` > **Information:** > > Il vous suffit de dupliquer les autres fichiers que nous avons créés pour chacun de vos projets, et les réglages propres à votre projet se feront ici, dans le fichier `.docker.env`. ### makefile {#makefile} Pour se simplifier la vie, nous allons regrouper un ensemble de commandes dans un fichier appelé `makefile`. Créez un fichier `makefile` avec le contenu ci-dessous : **makefile** ``` # cspell:ignore ifdef,pydocstyle,isort,mypy default: help # Folder where Python binaries are installed BIN=/usr/local/bin # Name of our Docker container CONTAINER=app_python # The project type DOCKER_PROJECT_TYPE=Python # Name of our Docker environment file DOCKER_ENV_FILE=.docker.env # The environment file we'll use ENV=--env-file ${DOCKER_ENV_FILE} # The list of Docker compose yaml file we'll use YAML=COMPOSE_FILE=compose.yaml COLOR_CYAN:=36 _CYAN := "\033[1;${COLOR_CYAN}m%s\033[0m %s\n" # Required for `make devcontainer`: we need to convert a string like "python" to # an hexadecimal string. (printf "python" | xxd -p will return 63325f66646d5f706870) DOCKER_VSCODE:=$(shell printf "${CONTAINER}" | xxd -p) .PHONY: bash bash: ## App - Start an interactive shell inside your application container ${YAML} docker compose ${ENV} exec -it ${CONTAINER} /bin/bash .PHONY: build build: ## App - Build a fresh Docker image for this project ${YAML} docker compose ${ENV} build ${ARGS} .PHONY: config config: ## App - Show the docker configuration ${YAML} docker compose ${ENV} config .PHONY: devcontainer devcontainer: ## App - Open the PHP container in vscode (you can f.i. directly start your debugging session) code --folder-uri vscode-remote://attached-container+${DOCKER_VSCODE}/app .PHONY: help help: ## App - Show the help with the list of commands @clear # Parse this file, search for `##` followed by a description @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[0;33m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @echo "" .PHONY: remove remove: ## App - Stop containers and remove them (you will lose updated files if you do not have locally) ${YAML} docker compose ${ENV} down --remove-orphans --rmi all --volumes .PHONY: up up: ## App - Based on the created Docker images (made with `make build`), create containers and start them ${YAML} docker compose ${ENV} up --detach ##@ Quality assurance .PHONY: doc-style doc-style: ## QA - pydocstyle is a static analysis tool for checking compliance with Python docstring conventions @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pydocstyle /app/src .PHONY: format format: ## QA - Format the script using Black @clear @${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/black /app/src .PHONY: lint lint: ## QA - Lint the script using Pylint - Run analyses your code without actually running it @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pylint /app/src .PHONY: mypy mypy: ## QA - Mypy is a program that will type check your Python code @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/mypy --cache-dir /tmp/mypy ``` > **Caution:** > > Si vous ne savez pas si `GNU make` est déjà installé, lancez `which make` dans la console. Si vous voyez `make not found`, lancez `sudo apt-get update && sudo apt-get install make` pour procéder à l'installation. ## Qu'avons-nous fait jusqu'ici ? {#what-have-we-done-so-far} Nous avons créé la structure minimale pour nos futurs projets Python. Nous avons défini notre image Docker (grâce au fichier `Dockerfile`) et la façon de l'utiliser (fichier `compose.yaml`). Nous avons aussi configuré notre projet (fichier `.docker.env`) et mis en place un certain nombre de commandes utiles (fichier `makefile`). Tous ces fichiers sont réutilisables dans l'ensemble de vos projets Python. ## Maintenant, créons notre environnement devcontainer {#now-lets-create-our-devcontainer-environment} Il nous faut créer un fichier supplémentaire. Créez un nouveau dossier appelé `.devcontainer` et, dedans, un fichier appelé `devcontainer.json`. Copiez/collez le contenu ci-dessous : **.devcontainer/devcontainer.json** ```json { "name": "app_python", "dockerComposeFile": [ "./../compose.yaml", ], "service": "app_python", "remoteUser": "python", "workspaceFolder": "/app", "customizations": { "vscode": { "extensions": [ "aaron-bond.better-comments", "DavidAnson.vscode-markdownlint", "eamodio.gitlens", "EricSia.pythonsnippets3", "foxundermoon.shell-format", "gruntfuggly.todo-tree", "mde.select-highlight-minimap", "mgesbert.python-path", "mikestead.dotenv", "mrmlnc.vscode-duplicate", "ms-azuretools.vscode-docker", "ms-python.black-formatter", "ms-python.debugpy", "ms-python.isort", "ms-python.mypy-type-checker", "ms-python.pylint", "ms-python.python", "ms-python.vscode-pylance", "ms-vscode-remote.remote-containers", "ms-vscode.makefile-tools", "njpwerner.autodocstring", "redhat.vscode-xml", "redhat.vscode-yaml", "sirtori.indenticator", "sonarsource.sonarlint-vscode", "streetsidesoftware.code-spell-checker-dutch", "streetsidesoftware.code-spell-checker-french", "streetsidesoftware.code-spell-checker", "tomoki1207.pdf", "tyriar.sort-lines" ], "settings": { "[dockerfile]": { "files.eol": "\n", "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[markdown]": { "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "always", "source.organizeImports": "always" } }, "[shellscript]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, "[xml]": { "editor.defaultFormatter": "redhat.vscode-xml", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml", "editor.insertSpaces": true, "editor.tabSize": 2 }, "cSpell.language": "en,fr,nl", "docker-explorer.enableTelemetry": false, "editor.bracketPairColorization.enabled": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, "editor.defaultFoldingRangeProvider": null, "editor.detectIndentation": false, "editor.folding": true, "editor.foldingStrategy": "auto", "editor.formatOnSave": true, "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", "editor.guides.highlightActiveIndentation": true, "editor.guides.indentation": true, "editor.multiCursorModifier": "ctrlCmd", "editor.renderWhitespace": "all", "editor.rulers": [ 120, 70 ], "editor.stickyScroll.enabled": true, "editor.tabCompletion": "on", "editor.tabSize": 4, "editor.wordBasedSuggestionsMode": "allDocuments", "editor.wordWrapColumn": 120, "explorer.compactFolders": false, "explorer.confirmDelete": true, "explorer.confirmDragAndDrop": true, "extensions.autoCheckUpdates": false, "files.autoSave": "onFocusChange", "files.defaultLanguage": "${activeEditorLanguage}", "files.eol": "\n", "files.exclude": { "**/.cache": true, "**/.git": true }, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "markdown.extension.toc.levels": "2..6", "markdownlint.config": { "MD033": false, "MD036": false }, "mypy-type-checker.args": [ "--config-file=.config/.mypy.ini" ], "python.analysis.autoFormatStrings": true, "python.analysis.autoImportCompletions": true, "python.analysis.autoIndent": true, "python.analysis.autoSearchPaths": true, "python.analysis.fixAll": [ "source.convertImportFormat" ], "python.analysis.typeCheckingMode": "strict", "python.defaultInterpreterPath": "/usr/local/bin/python3", "python.formatting.provider": "black", "python.languageServer": "Pylance", "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.pylintEnabled": true, "python.sortImports.args": [ "--profile", "black" ], "redhat.telemetry.enabled": false, "sonarlint.output.showAnalyzerLogs": true, "sonarlint.disableTelemetry": true, "sonarlint.rules": { "docker:S7031": { "level": "off" } }, "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.fontFamily": "MesloLGS NF", "update.enableWindowsBackgroundUpdates": false, "update.mode": "none", "yaml.format.enable": true, "yaml.completion": true, "yaml.validate": true, "workbench.editor.enablePreview": false, "workbench.editor.wrapTabs": true } } } } ``` Ce fichier est très long ; il pourrait être plus court, mais l'idée est de configurer VSCode. Nous avons donc listé toutes les extensions que nous voulons dans notre environnement de codage, ainsi qu'un paquet de settings. Notre installation est terminée. Voici la structure finale de notre projet : ![Our final project's structure](./images/final-structure.webp) ## Démarrer le projet dans un devcontainer {#start-the-project-in-a-devcontainer} Si VSCode est toujours ouvert, fermez-le. Si vous êtes toujours dans la console du container, tapez `exit` pour revenir à la console de votre host. Maintenant, lancez `make devcontainer` (donc sur votre machine hôte), qui est l'une des commandes déjà présentes dans notre `makefile`. VSCode démarre et ouvre le projet directement *dans* le container : ![VSCode - Devcontainer](./images/vscode_devcontainer.webp) VSCode va demander (voir en bas à droite) d'installer l'extension Python de Microsoft ; autorisez-le et cliquez sur `Install`. VSCode demandera aussi si vous voulez installer les extensions recommandées ; faites-le. ![Install recommended extensions](./images/vscode_install_extensions.webp) ## Conclusion {#conclusion} Vous avez maintenant un environnement de développement Python pleinement fonctionnel. Grâce à notre image Docker, Python a été installé et configuré pour tourner dans un container Docker (comprenez : rien n'a été installé sur votre machine) et, grâce au devcontainer, vous êtes sûr que VSCode est correctement configuré avec toutes les extensions nécessaires pour travailler sereinement. Vous êtes sous Windows et vous voulez exactement les mêmes fichiers, adaptés ? Voyez [Docker - Easy setup of Python under Windows](/blog/docker-python-devcontainer-windows). Vous préférez ne pas créer les fichiers à la main du tout ? [Docker - Even easier setup of Python](/blog/docker-python-devcontainer-microsoft) utilise l'assistant intégré de VSCode à la place. --- # Docker - Installation facile de Python sous Windows > Mettez en place facilement un environnement de développement Python sous Windows avec Docker et les devcontainers VSCode. Codez immédiatement, sans installer Python localement. --- ![Docker - Installation facile de Python sous Windows](/img/v2/devcontainer.webp) **TL;DR** Cet article porte sous Windows la configuration du devcontainer Python déjà vue sous Linux : les mêmes fichiers `Dockerfile`, `compose.yaml`, `.docker.env` et `.devcontainer/devcontainer.json` sont réutilisés tels quels, mais GNU Make est remplacé par un script batch DOS `make.bat`. Les commandes `make build`, `make up`, `make bash` et `make devcontainer` fonctionnent à l'identique depuis une console MS-DOS — vous obtenez un environnement de développement Python sans aucune installation locale de Python. Dans un [article précédent](/blog/docker-python-devcontainer), j'ai fourni quelques fichiers permettant de créer rapidement un environnement Python sous Linux. Aujourd'hui, amusons-nous à utiliser exactement les mêmes fichiers, mais cette fois sous Windows. Le défi du jour est simple : créer un environnement Python sur ma machine Windows sans installer Python, évidemment, et sans devoir configurer VSCode. Il suffit de lancer un peu de magie et, voilà, en tant que débutant en Python, je peux commencer à coder sans perdre de temps à configurer mon ordinateur. *Dès que vous y écrivez du vrai code, ajoutez les outils de [Python - Code Quality tools](/blog/python-qa) : ils tournent eux aussi dans le container, donc votre machine Windows reste intacte.* Dans l'article [Docker - Python devcontainer](/blog/docker-python-devcontainer), j'ai donc fourni quelques fichiers et nous allons les réutiliser, sans les modifier, dans cet article. Partons du principe qu'ils se trouvent déjà dans un dossier de projet sur votre machine et que vous avez ouvert une console MS-DOS positionnée dessus — voici ce qui se passe quand vous lancez les mêmes commandes que sous Linux, directement depuis DOS. ## C'est parti {#time-to-start} Toutes les commandes ci-dessous doivent être lancées depuis une console MS-DOS et vous devez vous trouver dans le dossier de votre projet (`cd %USERPROFILE%\Documents\Python`). ### Construire l'image Docker Python {#build-the-python-docker-image} La première chose à faire, une seule fois, est de créer l'image Docker. Cela se fait en lançant `make build` : ![Construction de l'image Docker](./images/make_build.webp) ### Créer un container Docker {#create-a-docker-container} Ensuite, une fois par jour, vous devez lancer `make up` : ![Créer un container Docker](./images/make_up.webp) Vous ne devez le faire qu'une fois par jour, car le container sera très probablement arrêté quand vous éteindrez votre ordinateur. Lancez simplement `make up` le lendemain matin pour le *réveiller*. ### Entrer dans le container {#entering-the-container} Si vous devez entrer dans votre container Docker (démarré par `make up`), lancez simplement `make bash`. Vous obtenez une console différente, comme illustré ci-dessous (voyez la baleine bleue, par exemple). La capture ci-dessous montre l'affichage du numéro de version de Python : ![Entrer dans le container](./images/make_bash.webp) Souvenez-vous des fichiers créés au chapitre précédent. L'un d'eux s'appelait `main.py` et contenait un script Python tout simple ; lançons-le : ![Exécution de main.py](./images/make_bash_main.webp) Il suffit de lancer le binaire `python` suivi du nom du script. > **Information:** > > Vous êtes donc bien à l'intérieur d'un container en cours d'exécution (voyez la baleine bleue). Tapez `exit` pour quitter le container (celui-ci continuera de tourner) et revenir à votre console MS-DOS. ## Démarrer VSCode et commencer votre aventure de développeur {#starting-vscode-and-start-your-developer-journey} Lancez simplement `make devcontainer` (dans votre console MS-DOS ; pas dans un container) et Visual Studio Code démarrera à l'intérieur de votre container Docker. Cette fonctionnalité s'appelle le *devcontainer*. ![Lancement de VSCode - Devcontainer](./images/make_devcontainer.webp) Dans VSCode, si vous aimez cette façon de travailler, vous pouvez appuyer sur `CTRL`+`` ` `` pour ouvrir un terminal (vous pouvez aussi cliquer sur le menu `View` puis sur l'entrée `Terminal` ; c'est la même chose). ![Utiliser le terminal de VSCode](./images/vscode_terminal.webp) Ici, si vous voulez exécuter le script `main.py`, tapez simplement `python src/main.py` et appuyez sur `Enter`. ![Exécution du script main.py depuis le terminal](./images/vscode_terminal_running.webp) > **Information:** > > Pour les utilisateurs Windows, notez que le container (et donc VSCode) tourne sous Linux. Il y a évidemment de nombreuses différences. Ce qu'il faut savoir, c'est que Linux utilise `/` comme séparateur de dossiers et non `\` comme DOS/Windows. C'est pourquoi nous avons tapé `src/main.py` et non `src\main.py`. ### Puis-je ajouter des extensions Python dans VSCode ? {#can-i-add-pythons-addons-in-vscode} Bien sûr, allez simplement dans la liste des extensions (appuyez sur `CTRL`+`SHIFT`+`X`) et... comme vous le voyez, certaines sont déjà installées. C'est l'un des avantages d'utiliser un devcontainer comme vous le faites en ce moment (voyez le fichier `.devcontainer/devcontainer.json` créé plus tôt). ![Extensions VSCode](./images/vscode_addons.webp) Je parle de magie, mais entrons maintenant dans le détail et voyons comment cette structure de fichiers/dossiers est réellement créée. Je pourrais vous fournir une archive ZIP, par exemple, avec tous les fichiers et la structure déjà prêts, mais ce serait moins... amusant, non ? ## Créer une structure de fichiers/dossiers {#create-a-filefolder-structure} Créez un dossier sur votre machine Windows, par exemple un dossier `Python` dans vos documents (en MS-DOS, voici la commande à lancer : `mkdir %USERPROFILE%\Documents\Python` puis pour y aller : `cd %USERPROFILE%\Documents\Python`). Vous pouvez bien sûr lancer l'Explorateur Windows, aller dans votre dossier *Documents* et créer le dossier et tous les fichiers ainsi : ![Explorateur Windows](./images/explorer.webp) Créez tous les fichiers nécessaires (ceux de l'article précédent). Nous n'avons PAS modifié ces fichiers : nous pouvons donc utiliser exactement les mêmes, que l'on soit sous Linux ou sous Windows. Ajoutez simplement un nouveau fichier dans votre dossier Windows et copiez/collez le contenu ci-dessous. Copiez/collez le contenu ci-dessous dans un fichier appelé `.devcontainer/devcontainer.json`. **.devcontainer/devcontainer.json** ```json { "name": "app_python", "dockerComposeFile": [ "./../compose.yaml", ], "service": "app_python", "remoteUser": "python", "workspaceFolder": "/app", "customizations": { "vscode": { "extensions": [ "aaron-bond.better-comments", "DavidAnson.vscode-markdownlint", "eamodio.gitlens", "EricSia.pythonsnippets3", "foxundermoon.shell-format", "gruntfuggly.todo-tree", "mde.select-highlight-minimap", "mgesbert.python-path", "mikestead.dotenv", "mrmlnc.vscode-duplicate", "ms-azuretools.vscode-docker", "ms-python.black-formatter", "ms-python.debugpy", "ms-python.isort", "ms-python.mypy-type-checker", "ms-python.pylint", "ms-python.python", "ms-python.vscode-pylance", "ms-vscode-remote.remote-containers", "ms-vscode.makefile-tools", "njpwerner.autodocstring", "redhat.vscode-xml", "redhat.vscode-yaml", "sirtori.indenticator", "sonarsource.sonarlint-vscode", "streetsidesoftware.code-spell-checker-dutch", "streetsidesoftware.code-spell-checker-french", "streetsidesoftware.code-spell-checker", "tomoki1207.pdf", "tyriar.sort-lines" ], "settings": { "[dockerfile]": { "files.eol": "\n", "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[markdown]": { "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "always", "source.organizeImports": "always" } }, "[shellscript]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, "[xml]": { "editor.defaultFormatter": "redhat.vscode-xml", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml", "editor.insertSpaces": true, "editor.tabSize": 2 }, "cSpell.language": "en,fr,nl", "docker-explorer.enableTelemetry": false, "editor.bracketPairColorization.enabled": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, "editor.defaultFoldingRangeProvider": null, "editor.detectIndentation": false, "editor.folding": true, "editor.foldingStrategy": "auto", "editor.formatOnSave": true, "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", "editor.guides.highlightActiveIndentation": true, "editor.guides.indentation": true, "editor.multiCursorModifier": "ctrlCmd", "editor.renderWhitespace": "all", "editor.rulers": [ 120, 70 ], "editor.stickyScroll.enabled": true, "editor.tabCompletion": "on", "editor.tabSize": 4, "editor.wordBasedSuggestionsMode": "allDocuments", "editor.wordWrapColumn": 120, "explorer.compactFolders": false, "explorer.confirmDelete": true, "explorer.confirmDragAndDrop": true, "extensions.autoCheckUpdates": false, "files.autoSave": "onFocusChange", "files.defaultLanguage": "${activeEditorLanguage}", "files.eol": "\n", "files.exclude": { "**/.cache": true, "**/.git": true }, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "markdown.extension.toc.levels": "2..6", "markdownlint.config": { "MD033": false, "MD036": false }, "mypy-type-checker.args": [ "--config-file=.config/.mypy.ini" ], "python.analysis.autoFormatStrings": true, "python.analysis.autoImportCompletions": true, "python.analysis.autoIndent": true, "python.analysis.autoSearchPaths": true, "python.analysis.fixAll": [ "source.convertImportFormat" ], "python.analysis.typeCheckingMode": "strict", "python.defaultInterpreterPath": "/usr/local/bin/python3", "python.formatting.provider": "black", "python.languageServer": "Pylance", "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.pylintEnabled": true, "python.sortImports.args": [ "--profile", "black" ], "redhat.telemetry.enabled": false, "sonarlint.output.showAnalyzerLogs": true, "sonarlint.disableTelemetry": true, "sonarlint.rules": { "docker:S7031": { "level": "off" } }, "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.fontFamily": "MesloLGS NF", "update.enableWindowsBackgroundUpdates": false, "update.mode": "none", "yaml.format.enable": true, "yaml.completion": true, "yaml.validate": true, "workbench.editor.enablePreview": false, "workbench.editor.wrapTabs": true } } } } ``` Copiez/collez le contenu ci-dessous dans un fichier appelé `src/main.py`. **src/main.py** ```python print("I'm your Python code") ``` **src/requirements.txt** ```txt Oui, un fichier vide... Le fichier doit être présent mais, pour l'instant, nous n'avons aucune dépendance à y mettre. ``` Copiez/collez le contenu ci-dessous dans un fichier appelé `.docker.env` (dans le monde Linux, ce type de fichier est caché puisqu'il commence par un point). **.docker.env** ```env # Application root directory in the container (PHP or NGINX) (--app-home) DOCKER_APP_HOME=/app # Name of the container to show in the bash prompt DOCKER_CONTAINER_NAME=app # Set OS groupid in your Docker Linux containers (1000 = root) (--os-groupid) DOCKER_OS_GROUPID=1000 # Set OS userid in your Docker Linux containers (1000 = root) (--os-userid) DOCKER_OS_USERID=1000 # Set OS username in your Docker Linux containers (--os-username) DOCKER_OS_USERNAME="python" # Version of Python to use DOCKER_PYTHON_VERSION=3.13-slim ``` Copiez/collez le contenu ci-dessous dans un fichier appelé `compose.yaml`. **compose.yaml** ```yaml name: app_python services: app_python: build: context: . target: development args: # Version of Python to use - DOCKER_PYTHON_VERSION=${DOCKER_PYTHON_VERSION} # Application root directory in the container (PHP or NGINX) (--app-home) - DOCKER_APP_HOME=${DOCKER_APP_HOME:-/app} # Name of the container to show in the bash prompt - DOCKER_CONTAINER_NAME=${DOCKER_CONTAINER_NAME:-python} # Set OS groupid in your Docker Linux containers (1000 = root) (--os-groupid) - DOCKER_OS_GROUPID=${DOCKER_OS_GROUPID:-1000} # Set OS userid in your Docker Linux containers (1000 = root) (--os-userid) - DOCKER_OS_USERID=${DOCKER_OS_USERID:-1000} # Set OS username in your Docker Linux containers (--os-username) - DOCKER_OS_USERNAME=${DOCKER_OS_USERNAME:-python} user: ${DOCKER_OS_USERID:-1000}:${DOCKER_OS_GROUPID:-1000} env_file: - .docker.env container_name: app_python volumes: # Our codebase on our host - .:${DOCKER_APP_HOME} # Keep installed VSCode extensions in a volume to avoid to reinstall them - vscode-extensions:/home/${DOCKER_OS_USERNAME}/.vscode-server/extensions # Remember the bash history - bashhistory:/home/${DOCKER_OS_USERNAME}/commandhistory volumes: # Use a Docker volume self-managed volume to store vscode's cache vscode-extensions: # Use a Docker volume self-managed volume to store bash history bashhistory: ``` Copiez/collez le contenu ci-dessous dans un fichier appelé `Dockerfile`. **Dockerfile** ```docker # syntax=docker/dockerfile:1 # cspell:ignore PYTHONDONTWRITEBYTECODE,PYTHONUNBUFFERED,HISTFILE # cspell:ignore addgroup,adduser,keyscan,hadolint,gecos,endregion # Those variables are initialized in the .docker.env file ARG DOCKER_APP_HOME="/app" ARG DOCKER_CONTAINER_NAME="python" ARG DOCKER_OS_GROUPID=1000 ARG DOCKER_OS_USERID=1000 ARG DOCKER_OS_USERNAME="python" ARG DOCKER_PYTHON_VERSION=3.10-slim # region - Our Python base image. We'll install Linux and Python dependencies here # and do some other configuration work FROM python:${DOCKER_PYTHON_VERSION} AS base # Prevents Python from writing pyc files. ENV PYTHONDONTWRITEBYTECODE=1 # Keeps Python from buffering stdout and stderr to avoid situations where # the application crashes without emitting any logs due to buffering. ENV PYTHONUNBUFFERED=1 ARG DOCKER_APP_HOME WORKDIR "${DOCKER_APP_HOME}/src" # hadolint ignore=DL3008 RUN --mount=type=cache,target=/var/cache/apk,rw \ set -e -x \ && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries..." \ && apt-get update -yqq \ && apt-get install -y --no-install-recommends bash git openssh-client tree \ && apt-get clean \ && rm -rf /tmp/* /var/lib/apt/lists/* # Install Python dependencies # # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them into # into this layer. RUN --mount=type=cache,target=/root/.cache/pip \ --mount=type=bind,source=src/requirements.txt,target=requirements.txt \ printf "\e[0;105m%s\e[0;0m\n" "Install Python dependencies" \ && python -m pip install --no-cache-dir -r requirements.txt # Keep the container running ENTRYPOINT ["tail", "-f", "/dev/null"] # endregion # region - Define our development image FROM base AS development ENV SHELL /bin/bash # Our user will be part of the root group since we're building the development image ARG DOCKER_OS_GROUPID ARG DOCKER_OS_USERID ARG DOCKER_OS_USERNAME RUN set -e -x \ && mkdir -p "/home/.vscode-server/bin" \ && mkdir -p "/home/.vscode-server/extensions" \ && mkdir -p "/home/.vscode-server/extensionsCache" \ && printf "\e[0;105m%s\e[0;0m\n" "Create our ${DOCKER_OS_USERNAME} application user" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/bin" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/extensions" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/.vscode-server/extensionsCache" \ # Create the application user home directory && mkdir -p "/home/${DOCKER_OS_USERNAME}" \ # Create our application user group && addgroup "${DOCKER_OS_USERNAME}" --gid "${DOCKER_OS_GROUPID}" \ # Create our application user && adduser \ --system \ --disabled-password \ --gecos "" \ --home "/home/${DOCKER_OS_USERNAME}" \ --uid "${DOCKER_OS_USERID}" \ "${DOCKER_OS_USERNAME}" \ # And, finally, set the correct permissions to the home folder of our user && chown -R "${DOCKER_OS_USERNAME}:${DOCKER_OS_USERNAME}" "/home/${DOCKER_OS_USERNAME}" USER "${DOCKER_OS_USERNAME}" ARG DOCKER_CONTAINER_NAME RUN /bin/bash -c "echo \"PS1='\n\e[0;33m🐳 ${DOCKER_CONTAINER_NAME} \e[0;32mDEV\e[0m - \e[0;36m$(whoami)\e[0m \w # '\" >> /home/${DOCKER_OS_USERNAME}/.bashrc" # Save the bash history in file /home/${OS_USERNAME}/commandhistory/.bash_history # Like this we'll be able to map that folder using a volume in our # compose.yaml file and then make the history persistent. RUN set -e -x \ && SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/home/${DOCKER_OS_USERNAME}/commandhistory/.bash_history" \ && mkdir -p "/home/${DOCKER_OS_USERNAME}/commandhistory" \ && touch "/home/${DOCKER_OS_USERNAME}/commandhistory/.bash_history" \ && echo "${SNIPPET}" >> "/home/${DOCKER_OS_USERNAME}/.bashrc" \ && echo "${SNIPPET}" >> "/home/${DOCKER_OS_USERNAME}/.bashrc" # endregion ``` ## Créer notre fichier batch DOS {#create-our-dos-batch-file} Voici la différence avec Linux : au lieu d'utiliser GNU Make, nous allons simplement utiliser un script batch MS-DOS. Nous l'appellerons `make.bat` afin de pouvoir utiliser des actions comme `make build` ou `make up` comme sous Linux ; mais cette fois, sous DOS. Créez ce fichier supplémentaire : **make.bat** ```bat @echo off cls if "%1"=="" goto :help set APP_NAME=app_python REM The code below is the hex representation of the APP_NAME here above i.e. "app_python" REM You can get that value by running "make bash" then 'python -c "print('app_python'.encode().hex())"' REM Needed by the "make devcontainer" action set DEV_CODE=6170705f707974686f6e if "%1"=="bash" goto bash if "%1"=="build" goto build if "%1"=="devcontainer" goto devcontainer if "%1"=="up" goto up if "%1"=="start" goto start "%2" call :error "Invalid action." goto :help :help echo Usage: %0 command echo. echo Available Commands: echo. echo bash Enter the Docker Python container echo build Build the Python Docker image (to be done only once) echo devcontainer Start VSCode and open the Docker container echo up Create and start the Docker container (to be done once a day if you've turned off your computer) echo start Start a Python script in the Docker container (call it like 'make start main.py') goto :eof :bash echo Entering in our %APP_NAME% Docker Python container... Type exit to quit the session. docker compose --env-file .docker.env exec -it %APP_NAME% /bin/bash goto :eof :build echo Building our Python Docker image docker compose --env-file .docker.env build goto :eof :devcontainer echo Start vscode and open the Docker container code --folder-uri vscode-remote://attached-container+%DEV_CODE%/app goto :eof :up echo Create our container... docker compose --env-file .docker.env up --detach goto :eof :start if "%2"=="" ( call :error "Please specify the name of the script to execute, f.i. 'make start main.py'" goto :eof ) docker compose --env-file .docker.env exec -it %APP_NAME% python %2 goto :eof :error REM Trim double quotes from the beginning and end of the message set "ERROR_MESSAGE=%*" set "ERROR_MESSAGE=%ERROR_MESSAGE:~1,-1%" echo ERROR - %ERROR_MESSAGE% echo. goto :eof :eof ``` ## Notre configuration actuelle {#our-configuration-right-now} À ce stade, voici notre projet dans VSCode : ![Le projet dans VSCode](./images/vscode_setup.webp) Et voici à quoi il ressemble dans l'Explorateur Windows : ![Explorateur Windows](./images/explorer.webp) Ouvrez une console MS-DOS (appuyez sur la touche `Windows` de votre clavier ou cliquez sur le menu Démarrer, puis tapez `cmd`) ![Lancer MS DOS](./images/cmd.webp) Ouvrez la console et allez dans le dossier de votre projet, c'est-à-dire lancez `cd %USERPROFILE%\Documents\Python`. Voici à quoi ressemble le projet maintenant : ![À quoi ressemble le projet sous DOS](./images/msdos.webp) ## Puis-je utiliser une autre version de Python ? {#can-i-use-another-version-of-python} Bien sûr, éditez simplement le fichier `.docker.env` et vous y trouverez cette ligne : `DOCKER_PYTHON_VERSION=3.13-slim`. Changez le numéro de version, enregistrez le fichier, puis lancez `make build`, ensuite `make up` et, enfin, `make devcontainer` pour tout reconstruire et utiliser la version plus récente. --- # Docker - Une installation de Python encore plus simple > Simplifiez votre développement Python. Ce guide montre comment utiliser Docker et les Devcontainers de Microsoft pour un environnement Python incroyablement facile à mettre en place et reproductible. --- ![Docker - Une installation de Python encore plus simple](/img/v2/devcontainer.webp) **TL;DR** Cet article montre la méthode la plus rapide à ce jour pour obtenir un devcontainer Python : avec Docker et l'extension Docker de VSCode installés, utilisez l'assistant intégré « Dev Containers: Add Dev Container Configuration files... » de la Command Palette pour générer automatiquement `.devcontainer/devcontainer.json` pour Python, puis cliquez sur « Reopen in Container » — aucun fichier à créer à la main. Rien à installer, rien à configurer, rien à créer au préalable. Dans [un article précédent](/blog/docker-python-devcontainer) (et sa [suite spécifique à Windows](/blog/docker-python-devcontainer-windows)), j'ai écrit à la main les fichiers `Dockerfile`, `compose.yaml` et `devcontainer.json`. Il s'avère que VSCode peut tout générer pour vous. Voici comment utiliser VSCode et Docker pour créer un environnement Python prêt à l'emploi, que vous soyez sous Windows, Linux ou Mac. *L'assistant est la voie la plus rapide, mais écrire les fichiers vous-même est ce qui vous permet de contrôler exactement ce qui entre dans l'image — et de garder les outils de développement hors de la production, comme expliqué dans [One Docker Image for Production and Devcontainers - The Clean Way](/blog/docker-prod-devcontainer).* **Les seuls prérequis sont :** 1. Vous devez avoir Docker installé sur votre système 2. Vous devez avoir Visual Studio Code 3. Vous avez installé l'extension Docker for Visual Studio Code dans VSCode Voici le résultat. Une fois le devcontainer démarré, créez un nouveau fichier appelé `main.py` avec cette ligne `print("Hello from your Python Devcontainer!")`. Sauvegardez le fichier. Cliquez dans le terminal et tapez `python main.py` pour exécuter le script et tadaaa : ![Exécution du script](./images/running_the_script.webp) Un environnement Python fonctionnel, terminal et interpréteur prêts, et pas un seul fichier créé à la main. Voici comment VSCode le construit pour vous. **Si vous avez ces trois choses, faites simplement ceci :** 1. Démarrez Visual Studio Code 2. Appuyez sur CTRL+SHIFT+P pour ouvrir la Command Palette. 3. Sélectionnez **Dev Containers: Add Dev Container Configuration files...** 4. Et suivez l'assistant : * Cherchez **Python** * Sélectionnez la version la plus récente de Python, actuellement `3.13-bookworm` * Pas besoin d'installer de fonctionnalités supplémentaires, appuyez juste sur Enter * Idem pour les fichiers optionnels ; pas nécessaires, appuyez juste sur Enter Cela fait, VSCode va créer un fichier appelé `.devcontainer/devcontainer.json`. ![VSCode a créé le fichier .devcontainer/devcontainer.json](./images/devcontainer_created.webp) Regardez en bas à droite, cliquez sur le bouton `Reopen in Container`. Selon la vitesse de votre ordinateur et si les éléments ont déjà été téléchargés, vous obtiendrez cet écran : ![VSCode et son terminal](./images/terminal.webp) Regardez la partie inférieure : une fenêtre de terminal s'est affichée et vous avez un prompt affichant `vscode -> /workspaces/python $`. Cliquez dans le terminal et tapez `python --version` : ![Version](./images/version.webp) --- # Une seule image Docker pour la production et les devcontainers - la méthode propre > Apprenez à concevoir une configuration Docker qui fournit une image de production sécurisée et minimale tout en permettant un développement local sans effort via les Devcontainers de VS Code, avec des overrides propres et une isolation des environnements --- ![Une seule image Docker pour la production et les devcontainers - la méthode propre](/img/v2/docker_workflow_prod_devcontainer.webp) **TL;DR** Cet article présente une méthode propre pour utiliser une seule image Docker à la fois pour les déploiements en production et pour les Devcontainers de VS Code. L'approche consiste à construire d'abord une image de production minimale et sécurisée, puis à l'étendre avec des outils de développement, des configurations utilisateur spécifiques et des overrides pour obtenir une image devcontainer séparée. Cela garantit un environnement de production léger avec une surface d'attaque réduite, tout en offrant une expérience de développement riche et isolée. Cet article est le fruit d'une intense réflexion : comment définir une image Docker destinée au déploiement en production de la manière la plus propre possible et, tout en évitant au maximum les copier/coller et autres répétitions, créer une image pour le développement en devcontainer. Autrement dit : je veux créer l'image la plus légère et la plus sécurisée possible pour le déploiement en production, mais je dois évidemment pouvoir développer cette image dans mon environnement VSCode. Et pour cela, il me faut mes outils de développement, un utilisateur Linux spécifique pour synchroniser les fichiers avec mon host et éviter les problèmes de permissions, etc. Après plusieurs tentatives, voici ce à quoi je suis arrivé, et qui semble répondre à ce besoin. > **Quelques concepts clés à garder en tête pendant la lecture de cet article:** > > 1. Nous devons séparer complètement la future production de notre développement (devcontainer). > 2. Il faudra créer l'image Docker de production avant de pouvoir utiliser le devcontainer. ## Que signifie une séparation parfaite des environnements ? {#what-does-a-perfect-separation-of-environments-mean} Notre premier réflexe devrait toujours être : avons-nous besoin de ceci (utilitaire, configuration, utilisateur, etc.) dans l'image Docker que nous allons déployer sur le serveur ? Quelques exemples : * Les outils de développement tels qu'un linter, un formateur, un [outil de qualité de code](/blog/php-jakzal-phpqa), un débogueur, etc. n'ont pas leur place dans l'image qui sera déployée. * La création d'un utilisateur qui permettra la synchronisation des fichiers avec l'host n'a pas sa place dans l'image qui sera déployée. * ... Notre image doit être propre, aussi efficace et légère que possible... Nous devons minimiser ce qu'on appelle la « surface d'attaque », c'est-à-dire réduire le nombre d'outils installés afin de réduire les risques de sécurité. Tout ce dont nous avons besoin pour notre développement devra donc faire partie d'une seconde image Docker, celle que nous utiliserons quand nous activerons la fonctionnalité devcontainer dans VSCode. C'est là que nous créerons notre utilisateur, ajouterons les outils de développement et activerons certaines configurations comme le débogage, etc. Si vous n'en avez jamais mis en place, [le développement PHP dans un devcontainer avec des outils de qualité de code préinstallés](/blog/vscode-devcontainer) détaille un devcontainer complet, de zéro. > **Donc, en résumé, nous aurons deux images Docker:** > > Une pour le déploiement et une seconde pour étendre la première avec des outils et des éléments de configuration supplémentaires. Cette dernière ne quittera jamais notre host et nous ne la construirons jamais nous-mêmes : nous laisserons VSCode le faire pour nous. ![Concept clé - Deux images](./images/production_devcontainer.webp) ## Le résultat {#the-result} Un seul Dockerfile, pas de copier-coller, et les deux images se construisent à partir exactement des mêmes instructions : un container de production minimal et rootless, et un devcontainer qui l'étend avec les outils nécessaires au développement. Voici le container de production qui répond sur `http://localhost:`8000 : > Screenshot — http\://localhost:8000 > > {"message":"Hello, FastAPI - PRODUCTION!"} ![L'image de production a été créée](./images/prod_image_created.webp) Les étapes ci-dessous construisent exactement cela, à partir d'un dossier vide, puis l'étendent en devcontainer. ## Étape 1 - Créer l'image Docker de production {#step-1---we-have-to-create-the-production-docker-image} Créez un nouveau dossier dans votre dossier temporaire pour notre exemple. ```bash $ mkdir -p /tmp/docker-prod-devcontainer && cd $_ ``` ### Créons nos fichiers de production {#lets-create-our-production-files} Créons quelques fichiers (rappelez-vous, pour l'image Docker de production). #### Le fichier .env de production {#the-production-env-file} Pour cet article, nous allons créer un script Python : il faudra donc préciser la version de Python souhaitée. C'est juste pour l'exemple mais, dans votre cas, ce `.env` servira à y placer tous les paramètres que vous voulez pour votre propre image de production. Donc, même si vous ne voyez pas encore pourquoi utiliser ce fichier dans votre cas, gardez-le, même si vous n'avez aucune variable à y mettre. **/tmp/docker-prod-devcontainer/.env** ``` APP_HOME="/app" PYTHON_VERSION=3.13 TIMEZONE="Europe/Brussels" ``` #### Le fichier Dockerfile de production {#the-production-dockerfile-file} Le fichier fourni est un exemple réutilisable. Dans cet article, à titre d'exemple, nous allons créer un petit script Python pour illustrer certaines choses. **/tmp/docker-prod-devcontainer/Dockerfile** ```docker # cspell:disable # Production Dockerfile ARG DOCKER_APP_HOME="/app" ARG PYTHON_VERSION=3.13 ARG TIMEZONE="Europe/Brussels" FROM python:${PYTHON_VERSION}-slim ARG TIMEZONE ENV TZ=${TIMEZONE} RUN --mount=type=cache,target=/var/cache/apt,rw \ set -eux \ && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries..." \ && apt-get update -yqq \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ locales \ tini \ && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen \ && locale-gen en_US.UTF-8 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 ARG DOCKER_APP_HOME WORKDIR "${DOCKER_APP_HOME}" COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ set -eux \ && printf "\e[0;105m%s\e[0;0m\n" "Install Python prod dependencies..." \ && pip install -r requirements.txt # Make sure installed dependencies are in the PATH ENV PATH="/root/.local/bin:$PATH" COPY . . ENTRYPOINT ["/usr/bin/tini", "--"] ENV PYTHONPATH="${DOCKER_APP_HOME}/src" CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "main:app"] ``` Regardez le script fourni : 1. Nous définissons la version de Python à utiliser 2. Nous configurons quelques variables d'environnement comme le fuseau horaire, ... 3. Nous installons les dépendances Linux nécessaires pour la production (**mais attention à ne pas ajouter ici les dépendances utiles uniquement au développement local**) 4. Comme il s'agit d'un exemple Python, nous copions le fichier `requirements.txt` dans l'image Docker et installons les packages 5. Nous copions tous nos fichiers dans l'image Docker (veillez à bien configurer votre fichier `.dockerignore`) 6. Et, enfin, pour l'image de production, nous initialisons le point d'entrée et la commande à conserver > **Caution:** > > Comme vous pouvez le voir, nous n'avons pas créé ici, dans l'image Docker, d'utilisateur non privilégié. Notre image de production viole donc une règle que nous avons tous lue : **votre image Docker doit être rootless**. > > C'est un concept clé ici : > > * L'utilisateur sera configuré au moment du déploiement de l'image, avec des outils comme Kubernetes par exemple, > * Quand nous utiliserons l'image Docker pour le développement local, l'UID/GID de l'utilisateur devra correspondre à celui du développeur (et nous ne savons pas maintenant si c'est `1000:1000` ou autre chose), > * L'utilisateur peut facilement être spécifié dans le fichier `compose.yaml`. #### Le fichier compose.yaml de production {#the-production-composeyaml-file} Le fichier `compose.yaml` est utilisé par Docker compose (qui fait partie de Docker) pour savoir comment construire votre image et comment créer votre container. C'est un fichier très important puisque nous y définirons toutes les configurations requises, nous y spécifierons notre fichier `.env` pour que Docker puisse traduire les variables en valeurs, ... Nous dirons aussi à Docker quel port du container en cours d'exécution doit être partagé avec notre host, ... **/tmp/docker-prod-devcontainer/compose.yaml** ```yaml name: my_sample services: app: build: context: . dockerfile: Dockerfile args: - DOCKER_APP_HOME=${APP_HOME:-/app} - PYTHON_VERSION=${PYTHON_VERSION:-3.13} - TIMEZONE=${TIMEZONE:-Europe/Brussels} image: my_sample_prod:latest container_name: app env_file: .env ports: - "8000:8000" user: "1001:1001" security_opt: - no-new-privileges:true ``` > **Caution:** > > Faites attention aux parties `user:` et `security_opt:`. > > Elles disent à Docker d'utiliser un utilisateur ayant l'UID/GID `1001` lors de la création d'un container basé sur notre image. Cet utilisateur... n'existait pas dans l'image, alors pourquoi ? > > C'est une astuce pour s'assurer que le container tourne en rootless, c'est-à-dire que l'utilisateur dans le container est non privilégié. > > Comme mesure de sécurité supplémentaire, nous avons la ligne `security_opt: - no-new-privileges:true` pour empêcher le container d'obtenir des privilèges additionnels. > > En bref : quand un container sera créé à partir de notre image, l'utilisateur ne pourra exécuter que ce que nous avons décidé pour lui, rien de plus. #### Le fichier requirements Python de production {#the-production-python-requirements-file} Nous avons vu une mention de ce fichier dans notre `Dockerfile` : comme nous utilisons Python comme exemple pour ce billet, nous avons besoin d'un fichier `requirements.txt` pour déclarer les dépendances dont notre script a besoin. Dans notre exemple, il nous faut les dépendances FastAPI : **/tmp/docker-prod-devcontainer/requirements.txt** ```txt fastapi gunicorn uvicorn[standard] ``` > **Information:** > > Regardez ce fichier encore une fois : nous n'avons, à proprement parler, que les dépendances nécessaires à la production. Si votre cas d'usage n'est pas un script Python, vous ne devez évidemment pas créer ce fichier `requirements.txt` (mais, dans ce cas, mettez aussi à jour le `Dockerfile` et supprimez cette partie). #### Le script Python de production {#the-production-python-script} Et enfin, pour la démo, nous allons créer un script Python FastAPI qui renverra une simple réponse JSON ; assez direct : **/tmp/docker-prod-devcontainer/src/main.py** ```python from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"message": "Hello, FastAPI - PRODUCTION!"} ``` #### Récapitulatif des fichiers de production {#summary-for-the-production-files} À ce stade, notre projet ressemble à ceci, soit cinq fichiers. ```tree expanded=true showJSX=false debug=false title="docker-prod-devcontainer" ├─ .env ├── Dockerfile ├── compose.yaml ├── requirements.txt └── src └── main.py ``` ### Création de l'image Docker de production {#creating-the-docker-production-image} Ouvrez une console Linux et exécutez la commande ci-dessous pour voir si tout va bien, c'est-à-dire s'il n'y a pas d'erreur et si, par exemple, notre fichier `.env` où nous avons spécifié la version de Python est bien récupéré et utilisé. ```bash $ docker compose config ``` Vous obtiendrez quelque chose comme ceci : **console output** ``` name: my_sample services: app: build: context: /tmp/docker-prod-devcontainer dockerfile: Dockerfile args: APP_HOME: /ap PYTHON_VERSION: "3.13" TIMEZONE: Europe/Brussels container_name: app environment: APP_HOME: /ap PYTHON_VERSION: "3.13" TIMEZONE: Europe/Brussels image: my_sample_prod:latest networks: default: null ports: - mode: ingress target: 8000 published: "8000" protocol: tcp security_opt: - no-new-privileges:true user: 1001:1001 networks: default: name: my_sample_default ``` > **Souvenez-vous de notre utilisateur 1001:** > > Regardez ci-dessus la ligne `user: 1001:1001`. Nous l'avons vu dans un chapitre précédent : quand le container sera créé, l'utilisateur actif sera `1001:1001` et celui-ci n'existait pas (nous ne l'avons pas créé dans notre `Dockerfile`) : notre container sera rootless ; exactement ce que nous voulons. Maintenant, nous allons construire et créer le container en exécutant la commande ci-dessous. > **EXTRÊMEMENT IMPORTANT - NOUS DEVONS CONSTRUIRE L'IMAGE D'ABORD:** > > Comme notre futur devcontainer sera dérivé de l'image de production, nous DEVONS la construire en premier : > > ```bash > $ docker compose build > ``` > > Cela garantit que VSCode récupérera une version locale de votre image. Si vous ne le faites pas, si vous n'avez pas encore construit l'image, Docker essaiera de la télécharger depuis Docker Hub (depuis internet) et vous obtiendrez probablement une erreur. > > Donc, encore une fois, avant d'aller plus loin dans ce tutoriel, exécutez d'abord `docker compose build` dans une console. ### Lancer un container de production {#running-a-production-container} Nous pouvons maintenant créer un container si nous le souhaitons, en exécutant la commande ci-dessous : ```bash $ docker compose up --detach ``` Comme notre projet de démo est une application Python FastAPI, lancez simplement un navigateur et allez à l'adresse `http://localhost : 8000` pour voir le message de FastAPI. La sortie est au format JSON : > Screenshot — http\://localhost:8000 > > {"message":"Hello, FastAPI - PRODUCTION!"} ### Et, pour être sûr, vérifions qui est l'utilisateur courant en PROD {#and-just-to-be-sure-checking-who-is-the-current-user-for-prod} Optionnel : exécutons une commande paramétrée dans le container pour voir qui est l'utilisateur courant : ```bash $ docker compose exec app /bin/sh -c 'echo "Container user ID is $(id -u) and his group ID is $(id -g)"' Container user ID is 1001 and his group ID is 1001 ``` Ceci, juste pour confirmer : lors de l'exécution de notre container, celui-ci tournera avec un utilisateur non privilégié grâce à notre fichier d'orchestration `compose.yaml`. ![L'image de production a été créée](./images/prod_image_created.webp) ## Étape 2 - Override (ou extension) de l'image de production pour obtenir une image prête pour le devcontainer {#step-2---override-or-extend-the-production-image-and-get-a-devcontainer-ready-one} ### Créons nos fichiers devcontainer {#lets-create-our-devcontainer-files} Créez maintenant un sous-dossier appelé `.devcontainer` avec les fichiers ci-dessous. #### Le fichier .devcontainer/.env {#the-devcontainerenv-file} Au cas où nous aurions besoin d'éléments de configuration supplémentaires, créons le fichier `.devcontainer/.env` (même vide). **/tmp/docker-prod-devcontainer/.devcontainer/.env** ``` ``` #### Le fichier .devcontainer/Dockerfile {#the-devcontainerdockerfile-file} L'un des fichiers les plus importants est le `.devcontainer/Dockerfile`, où nous ajouterons des éléments supplémentaires (dépendances Linux, bibliothèques Python, ...) pour nous offrir une meilleure expérience de développement. **/tmp/docker-prod-devcontainer/.devcontainer/Dockerfile** ```docker # syntax=docker/dockerfile:1.6 # .devcontainer/Dockerfile ARG OS_USERID=1000 ARG OS_GROUPID=1000 ARG OS_USERNAME="vscode" ARG DOCKER_APP_HOME="/app" FROM my_sample_prod:latest ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 USER root #! If you get an error during the execution of this block, make sure the "my_sample_prod" #! image isn't running with a non-root user. It should be "root". Rebuild the image to be sure. RUN --mount=type=cache,target=/var/cache/apt,rw \ set -eux \ && printf "\e[0;105m%s\e[0;0m\n" "Install required Linux binaries for devcontainer..." \ && apt-get update -yqq \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ bash \ bash-completion \ git \ curl \ ca-certificates \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # --- Create non-root user --- ARG OS_USERID ARG OS_GROUPID ARG OS_USERNAME RUN set -eux \ && if ! id -u "${OS_USERNAME}" >/dev/null 2>&1; then \ printf "\e[0;105m%s\e[0;0m\n" "Create devcontainer user ${OS_USERNAME} (${OS_USERID}:${OS_GROUPID})..." \ && groupadd -g ${OS_GROUPID} "${OS_USERNAME}" \ && useradd -u ${OS_USERID} -g ${OS_GROUPID} -m --shell /bin/bash --create-home "${OS_USERNAME}" \ && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}"; \ fi # Ensure vscode home folders exist and are writable. The .vscode-server folder is used by # VSCode while installing extensions in the container. That folder has to be owner by our user; not root. RUN set -eux \ && mkdir -p "/home/${OS_USERNAME}/.vscode-server/bin" \ && mkdir -p "/home/${OS_USERNAME}/.vscode-server/extensions" \ && mkdir -p "/home/${OS_USERNAME}/.vscode-server/data" \ && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}/.vscode-server" # ---------- Shell and history setup ----------------------------------------- # Configure bash to use this file RUN echo "export HISTFILE=/home/${OS_USERNAME}/.bash_history" >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'export HISTSIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'export HISTFILESIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'shopt -s histappend' >> "/home/${OS_USERNAME}/.bashrc" # Set up the shell prompt RUN set -eux && \ echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${OS_USERNAME}/.bashrc" # Welcome message and tips (make sure the message isn't yet in the file) RUN set -eux; \ BASHRC="/home/${OS_USERNAME}/.bashrc"; \ MARKER="# DEV CONTAINER WELCOME MESSAGE"; \ grep -qxF "$MARKER" "$BASHRC" || cat <> "$BASHRC" $MARKER echo -e "\n📘 Welcome to the Dev Container!" echo -e "Below are some useful commands:" echo -e "" echo -e " ✨ \e[32mpython -m debugpy --listen 0.0.0.0:5679 -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload\e[0m Start a hot-reloading preview in your browser." EOF ARG DOCKER_APP_HOME # Important: in the production image, the folder "DOCKER_APP_HOME" (/app most probably) is # owned by the root user. Here, for the devcontainer, the folder has to be owned by our # current user. RUN set -eux \ && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "${DOCKER_APP_HOME}" USER "${OS_USERNAME}" COPY requirements.txt . RUN --mount=type=cache,target=/home/${OS_USERNAME}/.cache/pip \ set -eux \ && cat requirements.txt \ && printf "\e[0;105m%s\e[0;0m\n" "Install Python dev dependencies..." \ && python -m pip install -r requirements.txt WORKDIR "${DOCKER_APP_HOME}" ``` Regardez le script fourni encore une fois : 1. Nous réutilisons l'image Docker créée précédemment (voir la ligne `FROM my_sample_prod:latest`) 2. Nous installons quelques outils Linux de dev comme `bash`, `curl`, `git`, ... dont nous n'avons pas besoin en prod 3. Maintenant, nous créons un utilisateur non-root en utilisant quelques paramètres comme `OS_USERID`, `OS_GROUPID` et `OS_USERNAME`. Nous créons aussi quelques dossiers personnalisés pour cet utilisateur 4. Nous configurons notre shell pour des choses comme le fichier d'historique, pour avoir un prompt personnalisé, ... 5. Nous nous assurons que les différents dossiers que nous devons manipuler appartiennent bien à notre utilisateur 6. Nous installons quelques dépendances Python de dev spécifiques (nécessaires pour notre exemple ici) Cette dernière étape n'est donc nécessaire que pour illustrer ce billet. Utilisez les vôtres. > **Attention à l'instruction FROM:** > > Permettez-moi d'insister encore une fois sur ce point. Pour l'instant, notre `Dockerfile` étend une image existante appelée `my_sample_prod:latest`. > > Avez-vous cette image sur votre host maintenant ? Si vous ne l'avez pas, VSCode, quand vous utiliserez la fonctionnalité *Open in Devcontainer*, essaiera d'abord de télécharger l'image depuis Docker Hub. > > Bon, peut-être que l'image y est hébergée et, dans ce cas, Docker la téléchargera depuis le Hub. Peut-être que cette image n'existe nulle part ailleurs que sur votre host. Peut-être qu'elle existe à la fois sur le Hub et sur votre machine. > > Beaucoup de questions, non ? Soyez-en simplement bien conscient ! > > Veillez à exécuter `docker compose build` dans votre dossier racine si vous devez : > > 1. Créer l'image locale > 2. Rafraîchir l'image locale parce que vous avez modifié vos fichiers Docker de production (comme vu au chapitre précédent). > > Donc, si ce n'est pas encore fait, exécutez `docker compose build` ! Vérifiez que `docker image ls | grep -i my_sample` dans votre console liste bien votre image. #### Le fichier .devcontainer/requirements.txt {#the-devcontainerrequirementstxt-file} Ce petit fichier est là uniquement à titre d'illustration. Dans ce billet, nous créons une petite application Python FastAPI et, dans le `.devcontainer/requirements.txt`, nous ajouterons quelques dépendances Python supplémentaires pour le dev, comme l'installation de l'outil `Debugpy`, c'est-à-dire le débogueur Python de Microsoft. **/tmp/docker-prod-devcontainer/.devcontainer/requirements.txt** ```txt debugpy ``` Bien sûr, si votre projet n'est pas en Python, ce fichier peut être supprimé et, dans ce cas, supprimez aussi les lignes associées du `.devcontainer/Dockerfile`. #### Le fichier .devcontainer/compose.yaml {#the-devcontainercomposeyaml-file} Comme pour l'image de production, nous devons ici dire à Docker comment l'image doit être construite et comment le container (celui de dev) doit être créé. **/tmp/docker-prod-devcontainer/.devcontainer/compose.yaml** ```yaml services: app: build: context: ./.devcontainer dockerfile: Dockerfile args: - DOCKER_APP_HOME=${APP_HOME:-/app} - OS_USERID=${LOCAL_UID:-1000} - OS_GROUPID=${LOCAL_GID:-1000} - OS_USERNAME=${LOCAL_USER:-vscode} container_name: my_sample_devcontainer_app ports: - 5679:5679 # Debugpy port env_file: - .env - .devcontainer/.env volumes: - .:${APP_HOME:-/app} - vscode-extensions:/home/${LOCAL_USER:-vscode}/.vscode-server/extensions - ./.devcontainer/history/.bash_history:/home/${LOCAL_USER:-vscode}/.bash_history:rw command: sleep infinity volumes: vscode-extensions: ``` Si vous regardez ce fichier `.devcontainer/compose.yaml`, nous configurons plusieurs choses : * La liste des arguments de build (`args`) doit mentionner l'id utilisateur, l'id de groupe et le nom de notre utilisateur. C'est important : quand nous travaillerons dans le devcontainer, tous les fichiers créés dans le container appartiendront à cet utilisateur spécifique. Si nous utilisons `johndoe` avec `1001` pour l'id utilisateur et l'id de groupe, les fichiers seront synchronisés ainsi sur votre host ; appartenant à `johndoe`, pas à vous. Si vous êtes l'utilisateur `1000:1000` localement, vous comprenez que les fichiers ne seront pas les vôtres et que vous aurez des problèmes de permissions. C'est quelque chose à éviter. Pour cela, nous devons configurer correctement le fichier `.devcontainer/compose.yaml`. Regardez la syntaxe : `${LOCAL_UID:-1000}`. Par défaut, le fichier assignera `1000` (qui est souvent le bon ID). Mais d'abord, Docker vérifiera si une variable appelée `LOCAL_UID` existe sur l'host. Si c'est le cas, cette valeur sera utilisée. Donc, si l'exécution de `id -u` (ou `id -g` pour l'id de groupe) ne donne pas `1000` sur votre host, veillez à créer la variable `LOCAL_UID` ou `LOCAL_GID` dans votre fichier `.bashrc` et à leur assigner la bonne valeur. * Comme notre exemple porte sur Python, nous exposons aussi le port standard de DebugPy grâce à la ligne `5679:5679` pour que, au démarrage d'une session de débogage, le container fonctionne. * Nous référençons aussi deux fichiers `.env`, celui du dossier racine (utilisé pour l'image Docker PROD) et celui du dossier `.devcontainer` * Nous ajoutons quelques volumes : * `.:${APP_HOME:-/app}` montera (= partagera) les fichiers de notre projet (dans VSCode) avec le dossier `${APP_HOME:-/app}`, c'est-à-dire là où les fichiers sont stockés dans l'image Docker PROD. Ainsi, les fichiers déjà copiés dans l'image Docker PROD seront simplement écrasés par ceux de notre session de dev (=> c'est là que la magie opère) * Nous demandons aussi à Docker de créer un volume auto-géré pour les `vscode-extensions`. Grâce à cette ligne, la première fois que nous créerons le devcontainer, VSCode installera les extensions mais, la fois suivante, grâce à cette ligne, VSCode ne les réinstallera pas, elles sont déjà présentes. Ce `vscode-extensions` sert à la persistance des données. * Et la ligne pour `./.devcontainer/history/.bash_history` conservera l'historique de vos lignes de commande (dans la console) dans votre projet. Ainsi, la prochaine fois que vous travaillerez dans le devcontainer, l'historique CLI sera déjà rempli avec ce que vous avez fait par le passé. #### Le fichier .devcontainer/devcontainer.json {#the-devcontainerdevcontainerjson-file} Et, enfin, le fichier de configuration pour VSCode. Dans VSCode, pour pouvoir exécuter une commande `Devcontainer:` (depuis la **Command Palette** (`CTRL`+`SHIFT`+`P`)), le fichier `.devcontainer/devcontainer.json` doit être présent et indiquer à VSCode quoi faire. **/tmp/docker-prod-devcontainer/.devcontainer/devcontainer.json** ```json { "name": "MySample Devcontainer", "dockerComposeFile": [ "../compose.yaml", "compose.yaml" ], "service": "app", "workspaceFolder": "/app", "remoteUser": "vscode", "containerUser": "vscode", "containerEnv": { "GIT_SSH_COMMAND": "ssh -i /home/vscode/.ssh/id_ed25519 -o 'UserKnownHostsFile=/dev/null'" }, "initializeCommand": "mkdir -p .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore", "customizations": { "vscode": { "settings": { "files.autoSave": "onFocusChange", "window.title": "${dirty}${rootName} ${separator} [DevContainer]", "workbench.colorTheme": "Visual Studio Light" }, "extensions": [ "ms-python.debugpy" ] } } } ``` 1. Nous devons utiliser l'entrée `dockerComposeFile` pour charger nos deux fichiers `compose.yaml`, celui de la racine (celui de prod) et celui du dossier `.devcontainer`. C'est nécessaire parce que nous voulons juste étendre le `compose.yaml` de production. Nous réutilisons tous les services et clés définis dans `../compose.yaml` et en ajoutons (extension) ou en modifions (override) simplement quelques-uns 2. Nous devons indiquer à VSCode quel dossier doit être ouvert dans notre session Devcontainer, ce doit être `/app` (doit correspondre à la variable `APP_HOME` de `.devcontainer/compose.yaml`) 3. Nous définissons le nom de notre remoteUser, ce sera `vscode` (doit correspondre à la variable `OS_USERNAME` de `.devcontainer/compose.yaml`) 4. Nous ajoutons aussi une variable d'environnement `GIT_SSH_COMMAND`. C'est optionnel mais cela nous permettra d'exécuter une commande `git` (comme `git pull`) depuis l'intérieur de notre devcontainer 5. La clé `initializeCommand` créera le dossier `.devcontainer/history` sur notre host (avec nous comme propriétaire) juste avant la création du container. Ainsi, nous pourrons y stocker notre fichier d'historique bash (voir l'entrée `volumes` dans `.devcontainer/compose.yaml`) 6. Et, enfin, dans la partie `customizations`, nous pouvons ajouter les settings et extensions VSCode que nous voulons configurer et installer dans notre Devcontainer. Vous pouvez ajouter beaucoup de settings / extensions ici si vous le souhaitez, selon le type de votre projet #### La structure de notre projet à ce stade {#our-projects-structure-right-now} À ce stade, notre projet ressemble à ceci, soit dix fichiers. ```tree expanded=true showJSX=false debug=false title="docker-prod-devcontainer/" ├── .devcontainer │ ├── .env │ ├── Dockerfile │ ├── compose.yaml │ ├── devcontainer.json │ └── requirements.txt ├── .env ├── Dockerfile ├── compose.yaml ├── requirements.txt └── src └── main.py ``` ### Création de l'image Devcontainer {#creating-the-devcontainer-image} Cette partie est purement optionnelle, c'est juste pour vérifier si tout est correctement rempli. Allez dans le dossier `.devcontainer` (`cd .devcontainer`) et exécutez cette commande : ```bash $ docker compose -f ../compose.yaml -f compose.yaml config ``` Vous obtiendrez quelque chose comme ceci : **console output** ``` name: my_sample services: app: build: context: /tmp/docker-prod-devcontainer/.devcontainer dockerfile: Dockerfile args: APP_HOME: /ap OS_GROUPID: "1000" OS_USERID: "1000" OS_USERNAME: vscode PYTHON_VERSION: "3.13" TIMEZONE: Europe/Brussels command: - sleep - infinity container_name: my_sample_devcontainer_app environment: APP_HOME: /ap PYTHON_VERSION: "3.13" TIMEZONE: Europe/Brussels image: my_sample_prod:latest networks: default: null ports: - mode: ingress target: 8000 published: "8000" protocol: tcp - mode: ingress target: 5679 published: "5679" protocol: tcp security_opt: - no-new-privileges:true user: 1001:1001 volumes: - type: bind source: /tmp/docker-prod-devcontainer target: /ap bind: create_host_path: true - type: volume source: vscode-extensions target: /home/vscode/.vscode-server/extensions volume: {} - type: bind source: /tmp/docker-prod-devcontainer/.devcontainer/history/.bash_history target: /home/vscode/.bash_history bind: create_host_path: true networks: default: name: my_sample_default volumes: vscode-extensions: name: my_sample_vscode-extensions ``` Vous avez eu une erreur ? Si oui, vous pouvez la lire et la gérer plus facilement maintenant. Vous pouvez regarder la configuration pour voir si tout semble correct, c'est-à-dire si vos variables ont bien été remplacées par les valeurs provenant des fichiers `.env`. ![Les deux images, prod et devcontainer, ont été créées](./images/both_created.webp) ## Ouvrir le container avec VSCode {#opening-the-container-using-vscode} Il est temps d'ouvrir le projet avec la fonctionnalité Devcontainer de VSCode. ```bash $ cd /tmp/docker-prod-devcontainer && code . ``` ![Ouverture de VSCode](./images/opening_vscode.webp) Si vous regardez en bas à droite de votre écran, vous verrez probablement cette popup : ![La popup devcontainer](./images/popup_devcontainer.webp) Cliquez simplement sur le bouton `Reopen in Container` ou, deuxième méthode, appuyez sur `CTRL`+`SHIFT`+`P` pour ouvrir la **Command palette** et sélectionnez l'option "Dev Containers: Rebuild Without Cache and Reopen in Container". VSCode fermera le projet et le rouvrira. Cela prendra plus de temps (la première fois) parce qu'il doit d'abord créer le container Docker. ![Le DevContainer est prêt](./images/devcontainer.webp) Si vous voyez l'image ci-dessus, vous êtes dans le container. J'ai configuré un thème clair juste pour faire la distinction nette entre l'host (thème noir) et le devcontainer (thème clair). En bas à droite, vous pouvez créer un nouveau Terminal : ![Création d'un nouveau terminal](./images/add_new_terminal.webp) Dans ce projet de démo, vous pouvez voir ce message ; une seule fois : ![Le message de bienvenue](./images/welcome_terminal.webp) L'idée est donc de fournir une aide contextuelle au développeur et de lui indiquer, ici, comment démarrer le serveur FastAPI avec le hot reload et Debugpy activé. ### Testons {#lets-test} Si nous retournons à l'URL `http://localhost : 8000`, nous obtenons toujours la réponse JSON (le container tourne donc bien). > Screenshot — http\://localhost:8000 > > {"message":"Hello, FastAPI - PRODUCTION!"} Mais maintenant, dans VSCode, si vous éditez le script `src/main.py` et mettez à jour le message Hello comme ci-dessous : ![Mise à jour du message Hello](./images/updated_main.webp) Il vous suffit d'enregistrer la modification, de revenir à la fenêtre du navigateur et de rafraîchir la page : > Screenshot — http\://localhost:8000 > > {"message":"Hello, FastAPI - I'm running from the Devcontainer!"} ### Déboguons {#lets-debug} Dans ce projet de démo, nous avons ajouté la dépendance Debugpy. Il nous suffit d'ajouter un fichier supplémentaire pour VSCode. **/tmp/docker-prod-devcontainer/.vscode/launch.json** ```json { "version": "0.2.0", "configurations": [ { "name": "Python: FastAPI", "type": "debugpy", "request": "attach", "connect": { "host": "localhost", "port": 5679, }, "justMyCode": true, "pathMappings": [ { "localRoot": "${workspaceFolder}/src", "remoteRoot": "." } ] } ] } ``` Pour être sûr que l'extension fonctionne correctement, fermez VSCode et rouvrez-le. Rouvrez aussi le DevContainer (maintenant, vous pouvez simplement choisir "Reopen in Devcontainer"). Une fois dans votre devcontainer, ouvrez le fichier `src/main.py`. Cliquez n'importe où sur la ligne de retour (ligne 7) et appuyez sur `F9` pour ajouter un point d'arrêt. ![Ajout d'un point d'arrêt](./images/adding_a_breakpoint.webp) Appuyez sur `F5` pour démarrer la session de débogage. Retournez au navigateur, rafraîchissez la page et VSCode s'arrêtera sur cette ligne précise. ![Réussi](./images/success.webp) ## Pour aller plus loin {#going-further} L'exemple ci-dessus utilise Python ; exactement la même approche à deux images est utilisée, langage par langage, dans [Docker - Python devcontainer](/blog/docker-python-devcontainer) et [le développement PHP dans un devcontainer avec des outils de qualité de code préinstallés](/blog/php-devcontainer). Et si le devcontainer reste une notion abstraite pour vous, commencez par [Docker - Explain me like I'm five - What's Docker for?](/blog/docker-definition-like-im-five). --- # Améliorez vos présentations avec Quarto, reveal.js, Decktape, Docker et les DevContainers > Créez un slideshow reveal.js avec Quarto, Decktape, Docker et les DevContainers. Écrivez votre contenu en Markdown. Exportez-le en HTML et en PDF (grâce à Decktape) --- ![Améliorez vos présentations avec Quarto, reveal.js, Docker et les DevContainers](/img/v2/quarto_reveal_docker.webp) **TL;DR** Cet article détaille comment créer et gérer des présentations reveal.js avec Markdown, Quarto, Docker et les DevContainers de VSCode. Il montre comment mettre en place un projet Quarto dans un devcontainer pour écrire ses slides en Markdown, les prévisualiser en direct et voir les modifications se répercuter automatiquement. Le workflow couvre aussi l'export de la présentation en site HTML statique et en PDF de qualité grâce à Decktape : une alternative fluide aux outils traditionnels. Cela fait des années, peut-être huit, que je n'ai plus utilisé PowerPoint pour une présentation. Cliquer, glisser, redimensionner, changer la résolution des slides, mince, tout recommencer... Dessiner des zones de texte, jouer avec les polices et les tailles, déplacer les éléments... Ah non, ça suffit ! Fini PowerPoint. J'en tremble encore. J'utilise Markdown pour toute ma documentation et mes exports (PDF, DOCX, etc.), il était donc logique de choisir [reveal.js](https://revealjs.com/), qui me permet d'écrire mes slides sans toucher la souris, juste en écrivant du Markdown. **Quick Jump:** [Voir le résultat](#the-result) | [Installation](#installation) ## Le résultat {#the-result} Une fois le devcontainer construit et le serveur de preview démarré (voir la section Installation ci-dessous), votre slideshow s'ouvre en direct dans le navigateur — écrit en Markdown, prévisualisé instantanément : ![Le slideshow tourne](./images/first_slide.webp) Facile, non ? ## Pourquoi ça fonctionne {#why-it-works} reveal.js est beau, simple et pratique. Il permet d'écrire des slideshows HTML depuis un seul fichier `.md`. Comme j'utilise [Quarto](https://quarto.org/) pour ma documentation et que Quarto sait générer un site reveal.js, quoi de plus naturel que de prolonger [un de mes articles précédents](/blog/quarto-devcontainer) et de me fixer cet objectif : avec seulement VSCode et Docker sur ma machine, programmer un devcontainer qui me permette d'écrire et de visualiser ma présentation reveal.js, mais aussi de l'exporter en site HTML statique pour pouvoir, par exemple, déployer ma présentation sur un serveur SFTP, et enfin de générer une version PDF de la présentation où chaque slide correspond à une page en mode paysage. * Quarto génère, depuis la même source Markdown, un slideshow reveal.js, un document Word, un PDF ou [une présentation PowerPoint](/blog/quarto-powerpoint) — une source, plusieurs sorties. * Le devcontainer isole toute la toolchain (Quarto, le serveur de preview, Decktape) — rien à installer sur l'host à part VSCode et Docker. * L'export en HTML ou en PDF de qualité impression est intégré via Decktape — aucun outil d'export supplémentaire à configurer. Partons à l'aventure. ## Installation {#installation} Assurez-vous donc d'avoir VSCode et Docker sur votre machine. ### Il nous faut un projet Quarto {#we-need-a-quarto-project} Pour cet article, l'idée est d'utiliser Quarto et non de créer un projet reveal.js directement *à la main*. En effet, Quarto nous permettra de faire bien plus de choses, comme exporter notre slideshow en document Word, en PDF ou même en [présentation PowerPoint](/blog/quarto-powerpoint), ou vers n'importe quel autre format supporté par Quarto. Nous utiliserons Quarto pour, aussi, pouvoir générer le projet sous forme de slideshow. #### Vous n'en avez pas encore {#you-dont-have-one-yet} Pour illustrer cet article, téléchargez le fichier [demo.zip](./files/demo.zip) que j'ai préparé. C'est un projet de slideshow prêt à l'emploi. Enregistrez le fichier dans un nouveau dossier sur votre disque, par exemple `/tmp/revealjs`, et dézippez-le (`unzip demo.zip`). Ensuite, rendez-vous dans le dossier fraîchement créé (`cd /tmp/revealjs/demo`) et lancez `code .` pour ouvrir le projet dans VSCode. Dans ce projet de démo, vous trouverez déjà un dossier `.devcontainer` avec les fichiers nécessaires que je décris au chapitre suivant. #### Vous en avez déjà un {#you-have-one} Si vous avez déjà un projet Quarto existant, rendez-vous dedans et lancez `code .` pour ouvrir le projet dans VSCode. Ajoutez ensuite les fichiers suivants à la racine de votre projet : **Project: /tmp/revealjs/demo** > **Note:** > > Lancez maintenant 'code .' pour ouvrir VSCode. Appuyez sur 'F1' et sélectionnez 'Dev containers: Rebuild and Reopen in Devcontainer' pour ouvrir le projet en tant que devcontainer **\_quarto.yml** ```yaml project: type: default render: ["*.qmd"] format: revealjs: css: style.css author: You ratio: "16:9" code-line-numbers: false center: true footer: https://www.yoursite.com/ navigation-mode: linear transition: convex preview-links: auto scrollable: true show-notes: false slide-level: 6 slide-number: c/t theme: default title-slide-attributes: data-background-image: images/background.webp data-background-size: cover ``` **.devcontainer/compose.yaml** ```yaml --- # cspell:ignore tinytex services: quarto-doc-base: image: quarto-devcontainer:latest build: context: . dockerfile: Dockerfile args: # We use default values here. The actual UID/GID mapping is handled # by the VS Code Dev Container CLI at runtime, overriding these defaults # when the container starts via the user: vscode directive in the Dockerfile. USER_UID: 1000 USER_GID: 1000 # Flags you'll need to adjust depending on your project INSTALL_CHROMIUM: "false" INSTALL_CSPELL: "false" INSTALL_PRECOMMIT_HOOKS: "false" user: root working_dir: /workspace volumes: - ..:/workspace:rw - ./history/.bash_history:/home/vscode/.bash_history:rw - /var/run/docker.sock:/var/run/docker.sock - type: bind source: ${HOME}/.quarto target: /home/vscode/.quarto - type: bind source: ${HOME}/.ssh/id_ed25519 target: /home/vscode/.ssh/id_ed25519 read_only: true # Base command to keep the container running indefinitely command: /bin/sh -c "while sleep 1000; do :; done" ``` **.devcontainer/devcontainer.json** ```json { "name": "Quarto Docs Project - Demo", "dockerComposeFile": "compose.yaml", "service": "quarto-doc-base", "workspaceFolder": "/workspace", "remoteUser": "vscode", "containerEnv": { "LOCAL_WORKSPACE": "${localWorkspaceFolder}" }, "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { "version": "latest", "enableNonRootDocker": "true", "moby": "true" }, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/paul-gilber/devcontainer-features/yamllint-homebrew:1": {} }, "initializeCommand": "mkdir -p .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore", "postAttachCommand": "if which pre-commit >/dev/null 2>&1; then pre-commit install || true; fi", "forwardPorts": [8000, 5931], "customizations": { "vscode": { "settings": { "[dockerfile]": { "files.eol": "\n", "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[markdown]": { "editor.defaultFormatter": "yzhang.markdown-all-in-one", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml" }, "cSpell.language": "en,fr,nl", "cSpell.words": [ "Quarto", "devcontainer", "frontmatter", "Pandoc", "Gitlens" ], "editor.formatOnSave": true, "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", "editor.guides.highlightActiveIndentation": true, "editor.guides.indentation": true, "editor.multiCursorModifier": "ctrlCmd", "editor.renderWhitespace": "all", "editor.rulers": [120, 70], "editor.stickyScroll.enabled": true, "editor.tabCompletion": "on", "editor.tabSize": 4, "editor.wordBasedSuggestionsMode": "allDocuments", "editor.wordWrapColumn": 120, "explorer.compactFolders": false, "explorer.confirmDelete": true, "explorer.confirmDragAndDrop": true, "extensions.autoCheckUpdates": false, "files.associations": { "resources/*.json": "jsonc" }, "files.autoSave": "onFocusChange", "files.defaultLanguage": "${activeEditorLanguage}", "files.eol": "\n", "files.exclude": { "**/.cache": true, "**/.git": true, "**/.quarto": true, "**/*.html": true, "**/readme_files/**": true, "**/site_libs/**": true }, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "maptz.regionfolder": { "[dockerfile]": { "foldEnd": "[\\S]*\\#[\\s]*endregion", "foldEndRegex": "[\\S]*\\#[\\s]*endregion[\\s]*(.*)$", "foldStart": "[\\S]*\\#[\\s]*region [NAME]", "foldStartRegex": "[\\S]*\\#[\\s]*region[\\s]*(.*)$" } }, "markdown.extension.toc.levels": "2..6", "markdown.extension.tableFormatter.enabled": true, "markdownlint.config": { "MD033": false, "MD036": false }, "quarto.preview.renderOnSave": true, "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.fontFamily": "MesloLGS NF", "terminal.integrated.tabs.defaultLocation": "editorGroup", "yaml.schemas": { "https://json.schemastore.org/json-schema-v7.json": [ "_quarto.yml", "_quarto-*.yml" ], "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml" }, "window.title": "${dirty}${rootName} ${separator} [DevContainer]", "workbench.colorTheme": "Visual Studio Light" }, "extensions": [ "DavidAnson.vscode-markdownlint", "esbenp.prettier-vscode", "gitkraken.gitlens", "maptz.regionfolder", "mde.select-highlight-minimap", "mikestead.dotenv", "ms-vscode.makefile-tools", "quarto.quarto", "redhat.vscode-yaml", "sirtori.indenticator", "streetsidesoftware.code-spell-checker-dutch", "streetsidesoftware.code-spell-checker-french", "streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one" ] } } } ``` **.devcontainer/Dockerfile** ```docker # syntax=docker/dockerfile:1.4 # cspell:disable FROM ghcr.io/quarto-dev/quarto:1.8.25 AS base ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=1000 ARG INSTALL_CHROMIUM="false" ARG INSTALL_CSPELL="false" ARG INSTALL_PRECOMMIT_HOOKS="false" # Yaml tooling ARG YQ_VERSION=v4.42.1 ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 # ---------- Stage 1: Install system dependencies ---------- # # texlive-* : to render as PDF using "quarto render . --to pdf" # texlive-luatex : to ensure lualatex is installed RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt \ set -eux; \ apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ curl \ libnss3 libfontconfig1 libgbm1 \ libsecret-1-dev \ libx11-dev \ libxkbfile-dev \ openssh-client \ sudo \ unzip \ # yamllint \ # --- PDF Rendering Packages (Revised) --- texlive-xetex \ texlive-fonts-recommended \ texlive-plain-generic \ texlive-luatex \ texlive-latex-extra \ texlive-fonts-extra \ texlive-science \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Pre-generate the LuaLaTeX font database to avoid runtime delay # This command must be run AFTER all texlive packages are installed. # RUN set -eux; \ # luaotfload-tool -f -u -v # Install yq (YAML validator) for validating YAML frontmatter RUN --mount=type=cache,target=/var/cache/yq \ set -eux; \ curl -L --fail --silent --show-error \ https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 \ -o /var/cache/yq/yq_linux_amd64; \ install -m 0755 /var/cache/yq/yq_linux_amd64 /usr/local/bin/yq # ---------- Stage 2: Extras - pre-commit, Node.js + cspell ---------- FROM base AS extras ARG INSTALL_CSPELL ARG INSTALL_PRECOMMIT_HOOKS ARG USERNAME # Install pre-commit (without installing hooks at build time) RUN --mount=type=cache,target=/root/.cache/pip \ if [ "$INSTALL_PRECOMMIT_HOOKS" = "true" ]; then \ set -eux; \ echo "Installing Python and pre-commit..." && \ apt-get update && \ apt-get install -y --no-install-recommends python3-pip && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ pip3 install --no-cache-dir --upgrade pip && \ pip3 install --no-cache-dir pre-commit && \ pip cache purge || true; \ fi # Install Node.js + cspell (optional) RUN --mount=type=cache,target=/home/${USERNAME}/.npm \ if [ "${INSTALL_CSPELL}" = "true" ]; then \ set -eux; \ echo "Installing NPM and Code Spell Checker..." && \ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ npm config set cache /home/${USERNAME}/.npm && \ npm install -g cspell && \ npm cache clean --force && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*; \ fi # ---------- Stage 3: Finalization ---------- FROM extras AS final ARG USERNAME ARG USER_UID ARG USER_GID ARG INSTALL_CHROMIUM # ---------- User and permissions -------------------------------------------- RUN set -eux && \ USER_UID=${USER_UID:-1000} && \ USER_GID=${USER_GID:-1000} && \ groupadd -g "${USER_GID}" "${USERNAME}" && \ useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash "${USERNAME}" && \ chown -R "${USERNAME}":"${USERNAME}" "/home/${USERNAME}" # Grant passwordless sudo access to the node user # Allow our node user to run "sudo xxx" in case of very specific use case RUN set -eux && \ echo "${USERNAME} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/"${USERNAME}" && \ chmod 0440 /etc/sudoers.d/"${USERNAME}" # Switch to non-root user for Chromium and Quarto tools USER ${USERNAME} # Install Quarto Chromium tool (optional) RUN --mount=type=cache,target=/home/${USERNAME}/.quarto/tools \ if [ "${INSTALL_CHROMIUM}" = "true" ]; then \ set -eux; \ echo "Installing Quarto's Chromium tool..." && \ quarto install tool chromium; \ fi # ---------- Shell and history setup ----------------------------------------- # Note: Decktape is started on the host (using Docker-out-of-Docker). We need to # make sure the slides.pdf file is created with the right ownership and in the right place; our local absolute path. RUN set -eux \ && echo "alias decktape='docker run --rm --user root -v \${LOCAL_WORKSPACE}/:/slides ghcr.io/astefanutti/decktape reveal http://host.docker.internal:5931/ slides.pdf && sudo chown ${USERNAME}:${USERNAME} slides.pdf'" >> "/home/${USERNAME}/.bashrc" \ && echo "alias cspell='cspell lint . --config .vscode/cspell.json'" >> "/home/${USERNAME}/.bashrc" \ && echo "alias pre-commit='pre-commit run --all-files'" >> "/home/${USERNAME}/.bashrc" # Configure bash to use this file RUN echo "export HISTFILE=/home/${USERNAME}/.bash_history" >> "/home/${USERNAME}/.bashrc" && \ echo 'export HISTSIZE=10000' >> "/home/${USERNAME}/.bashrc" && \ echo 'export HISTFILESIZE=10000' >> "/home/${USERNAME}/.bashrc" && \ echo 'shopt -s histappend' >> "/home/${USERNAME}/.bashrc" # Set up the shell prompt RUN set -eux && \ echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${USERNAME}/.bashrc" # Welcome message and tips RUN set -eux && \ cat <> "/home/${USERNAME}/.bashrc" echo -e "\n📘 Welcome to the Quarto Docs Dev Container!" echo -e "Below are some useful commands:" echo -e "" echo -e " \e[1mQuarto Commands\e[0m" echo -e " ───────────────" echo -e " ✨ \e[32mquarto preview . --port 5931 --host 0.0.0.0\e[0m Start a hot-reloading preview in your browser." echo -e " ✨ \e[32mquarto render . --output-dir build\e[0m Render a static HTML site in the folder build." echo -e "" echo -e " ✨ \e[32mdecktape\e[0m Export the slideshow as slides.pdf with full respect of the layout." echo -e "" EOF # Append conditional commands depending on optional tools RUN set -eux && \ if [ "${INSTALL_CSPELL}" = "true" ] || [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \ echo 'echo -e " \e[1mData quality checks\e[0m"' >> "/home/${USERNAME}/.bashrc" && \ echo 'echo -e " ───────────────────"' >> "/home/${USERNAME}/.bashrc"; \ fi && \ if [ "${INSTALL_CSPELL}" = "true" ]; then \ echo 'echo -e " ✍️ \e[32mcspell\e[0m Run spell checks.\n"' >> "/home/${USERNAME}/.bashrc"; \ fi && \ if [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \ echo 'echo -e " ⚙️ \e[32mpre-commit\e[0m Run code quality checks on all files.\n"' >> "/home/${USERNAME}/.bashrc"; \ fi ``` > **Les images ci-dessous sont celles de mon projet ; certaines seront forcément différentes chez vous.:** ### Ouvrir le devcontainer {#open-the-devcontainer} Dans VSCode, appuyez sur `F1` pour ouvrir la **Command Palette** puis sélectionnez **Dev containers: Rebuild and Reopen in Devcontainer**. *Si vous n'avez pas cette commande, installez l'extension VSCode [Dev Container de Microsoft](https://marketplace.visualstudio.com/publishers/Microsoft).* VSCode va donc *reconstruire et réouvrir le projet en tant que devcontainer* : autrement dit, VSCode va d'abord construire une image Docker spécifique (telle que décrite dans le fichier `.devcontainer/Dockerfile`) et probablement télécharger d'abord l'image Docker de Quarto ; puis, une fois l'image créée, VSCode va créer un container basé sur cette image et enfin réouvrir le projet dans un environnement particulier appelé *un devcontainer*. Le container va se construire (comptez 1 à 2 minutes la première fois). Une fois le build terminé, VS Code affiche un écran d'accueil comme celui-ci : ![Construction du devcontainer](./images/building_devcontainer.webp) En cliquant sur le lien `Connecting to Dev Container (show log)`, vous obtiendrez plus de détails et pourrez suivre ce qui se passe. Après une ou deux minutes (cela n'arrive que la première fois), vous obtiendrez un écran comme celui-ci : ![Liste des commandes](./images/cheatsheet.webp) > **Cliquez sur le bouton `+`:** > > Si vous n'avez pas l'écran **Terminal** comme sur l'image ci-dessus, regardez à droite de l'écran et cliquez sur le bouton `+` pour créer un nouveau terminal bash. ## Plus de démos {#more-demos} ### Prévisualiser le slideshow {#preview-the-slideshow} Regardez la liste des commandes affichée dans le Terminal : il y en a une pour la preview. Lancez-la simplement comme suggéré, donc `quarto preview . --port 5931 --host 0.0.0.0`. ```bash quarto preview . --port 5931 --host 0.0.0.0 ``` Appuyez sur `Enter` pour démarrer le serveur de preview. VS Code vous demandera l'autorisation d'ouvrir le site dans votre navigateur. ![Ouverture du site](./images/external_website.webp) Et, presque immédiatement, VSCode lancera votre navigateur et créera un nouvel onglet avec votre slideshow déjà actif — le même résultat que celui montré en haut de cet article. #### Mettre à jour le slideshow {#updating-the-slideshow} Comme vous le voyez, mon slideshow est en français. Traduisons-le. Utilisez l'outil de traduction que vous voulez, moi j'utilise [DeepL](https://www.deepl.com/). Copiez un peu de contenu et traduisez-le en anglais. Retournez sur votre slideshow, rafraîchissez la page et... tadaaaaa, votre contenu est maintenant en anglais. ![Slide traduite](./images/translated_slide.webp) Comme nous venons de le voir, les modifications de votre contenu sont automatiquement répercutées dans le navigateur. > **Aller plus loin avec reveal.js:** > > Une fois votre slideshow lancé, jetez un œil à [Some tips and tricks for Quarto when rendering as a reveal.js slideshow](/blog/quarto-revealjs-tips) pour affiner le rendu. ### Exporter un PDF {#export-a-pdf} Maintenant que votre slideshow tourne, retournez dans votre Terminal (appuyez sur `CTRL`+`ù` pour l'ouvrir) et créez une nouvelle session Bash (cliquez sur le bouton `+`). Là, lancez `decktape` dans le Terminal pour générer un fichier appelé `slides.pdf` : ```bash $ decktape ``` Cela va générer un joli fichier PDF (bien meilleur que celui produit par la fonction d'export PDF de reveal.js). ### Exporter en site HTML statique {#export-as-static-html-site} En lançant `quarto render . --output-dir build`, vous créerez un dossier `build` contenant votre slideshow : ```bash $ quarto render . --output-dir build ``` > **Envie d'essayer ?:** > > Sur votre host, rendez-vous dans le dossier `build` et lancez `docker run -d --name static-site -p 8080:80 -v .:/usr/local/apache2/htdocs/ httpd:alpine` pour [faire tourner le slideshow](/blog/docker-html-site) en dehors de votre devcontainer. > > C'est un moyen simple de vérifier que ça fonctionne. Vous pouvez désormais lancer votre client SFTP, par exemple, et uploader le dossier `build` sur votre serveur web si c'était votre objectif. ## Conclusion {#conclusion} Du Markdown en entrée, un slideshow prévisualisé en direct en sortie, et un PDF ou un site HTML statique quand vient le moment de livrer — tout le rituel PowerPoint (cliquer, glisser, redimensionner) a disparu, remplacé par le même workflow de devcontainer que celui utilisé pour tout le reste sur ce blog. Si vous n'avez jamais mis en place un devcontainer Quarto, [Make your Quarto project Devcontainer-Ready — No More Setup Headaches](/blog/quarto-devcontainer) détaille davantage ces fondations. ---