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
Traduction automatique
Cet article a plus d'un an. Les informations peuvent être obsolètes.
Cet article fait partie de la série Développer dans un devcontainer :
- Développement PHP dans un devcontainer avec des outils de qualité de code préinstallés
- Installer un environnement PHP Docker en quelques secondes
- Docker - Python devcontainer
- Docker - Installation facile de Python sous Windows
- Docker - Une installation de Python encore plus simple
- Une seule image Docker pour la production et les devcontainers - la méthode propre
- Améliorez vos présentations avec Quarto, reveal.js, Decktape, Docker et les DevContainers

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
- Nous devons séparer complètement la future production de notre développement (devcontainer).
- Il faudra créer l'image Docker de production avant de pouvoir utiliser le devcontainer.
Que signifie une séparation parfaite des environnements ?
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, 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 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.

Le résultat
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 :
{"message":"Hello, FastAPI - PRODUCTION!"}

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
Créez un nouveau dossier dans votre dossier temporaire pour notre exemple.
Créons nos fichiers de production
Créons quelques fichiers (rappelez-vous, pour l'image Docker de production).
Le fichier .env de production
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.
APP_HOME="/app"PYTHON_VERSION=3.13TIMEZONE="Europe/Brussels"Ce script définit trois variables d'environnement essentielles pour configurer une application : le répertoire racine, la version de Python à utiliser, et le fuseau horaire. Ces valeurs servent probablement de configuration de base pour l'exécution d'un conteneur ou d'un environnement d'application.
Le fichier Dockerfile de production
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.
# cspell:disable
# Production Dockerfile
ARG DOCKER_APP_HOME="/app"ARG PYTHON_VERSION=3.13ARG 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 PATHENV 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"]Ce Dockerfile crée une image de production basée sur Python 3.13 qui installe les dépendances système requises (locales, tini), configure la langue et le fuseau horaire, installe les dépendances Python à partir d'un requirements.txt, puis lance une application avec gunicorn et uvicorn. L'image utilise des montages de cache pour optimiser les couches Docker et éviter de réinstaller les paquets à chaque build.
Regardez le script fourni :
- Nous définissons la version de Python à utiliser
- Nous configurons quelques variables d'environnement comme le fuseau horaire, ...
- 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)
- Comme il s'agit d'un exemple Python, nous copions le fichier
requirements.txtdans l'image Docker et installons les packages - Nous copions tous nos fichiers dans l'image Docker (veillez à bien configurer votre fichier
.dockerignore) - Et, enfin, pour l'image de production, nous initialisons le point d'entrée et la commande à conserver
Attention
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:1000ou autre chose), - L'utilisateur peut facilement être spécifié dans le fichier
compose.yaml.
Le fichier compose.yaml de production
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, ...
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:trueCe fichier Docker Compose définit un service nommé 'app' qui construit une image Docker à partir du Dockerfile du répertoire courant, en passant plusieurs variables de configuration (chemin d'accueil, version Python, fuseau horaire). Le service expose le port 8000, utilise un fichier .env pour les variables d'environnement, et s'exécute avec des restrictions de sécurité pour limiter les privilèges du conteneur.
Attention
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
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 :
fastapi
gunicorn
uvicorn[standard]
Ce fichier liste les dépendances Python nécessaires pour exécuter une application web avec FastAPI. FastAPI est le framework principal pour créer l'API, uvicorn[standard] est le serveur ASGI qui exécute l'application, et gunicorn est un gestionnaire de processus qui permet de déployer et de gérer plusieurs instances du serveur en production.
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
Et enfin, pour la démo, nous allons créer un script Python FastAPI qui renverra une simple réponse JSON ; assez direct :
from fastapi import FastAPI
app = FastAPI()
@app.get("/")def read_root(): return {"message": "Hello, FastAPI - PRODUCTION!"}Ce code crée une application FastAPI minimale avec une seule route GET qui retourne un message JSON. Lorsqu'on accède à la racine du serveur, la fonction read_root() répond avec un dictionnaire contenant un message de bienvenue.
Récapitulatif des fichiers de production
À ce stade, notre projet ressemble à ceci, soit cinq fichiers.
docker-prod-devcontainer
Création de l'image Docker de production
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é.
Vous obtiendrez quelque chose comme ceci :
name: my_sampleservices: 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:1001networks: default: name: my_sample_defaultCe fichier Docker Compose définit un service appelé app qui construit une image Docker à partir d'un Dockerfile, configure des variables d'environnement pour Python 3.13 et le fuseau horaire, expose le port 8000, et applique des restrictions de sécurité comme l'exécution sans privilèges élevés.
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 :
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
Nous pouvons maintenant créer un container si nous le souhaitons, en exécutant la commande ci-dessous :
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 :
{"message":"Hello, FastAPI - PRODUCTION!"}
Et, pour être sûr, vérifions qui est l'utilisateur courant en PROD
Optionnel : exécutons une commande paramétrée dans le container pour voir qui est l'utilisateur courant :
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.

Étape 2 - Override (ou extension) de l'image de production pour obtenir une image prête pour le devcontainer
Créons nos fichiers devcontainer
Créez maintenant un sous-dossier appelé .devcontainer avec les fichiers ci-dessous.
Le fichier .devcontainer/.env
Au cas où nous aurions besoin d'éléments de configuration supplémentaires, créons le fichier .devcontainer/.env (même vide).
Ce script bash configure un environnement de développement en installant des dépendances système via le gestionnaire de paquets, puis en téléchargeant et compilant les sources d'un projet. Il crée les répertoires nécessaires et gère les erreurs pour s'assurer que chaque étape réussit avant de continuer.
Le fichier .devcontainer/Dockerfile
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.
# syntax=docker/dockerfile:1.6
# .devcontainer/Dockerfile
ARG OS_USERID=1000ARG OS_GROUPID=1000ARG 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_USERIDARG OS_GROUPIDARG 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 fileRUN 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 promptRUN 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 <<EOF >> "$BASHRC"$MARKERecho -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}"Ce Dockerfile crée une image de conteneur de développement basée sur une image de production existante. Il installe les outils de développement nécessaires, crée un utilisateur non-root pour des raisons de sécurité, configure l'environnement bash avec l'historique et des invites personnalisées, puis installe les dépendances Python à partir d'un fichier requirements.txt.
Regardez le script fourni encore une fois :
- Nous réutilisons l'image Docker créée précédemment (voir la ligne
FROM my_sample_prod:latest) - Nous installons quelques outils Linux de dev comme
bash,curl,git, ... dont nous n'avons pas besoin en prod - Maintenant, nous créons un utilisateur non-root en utilisant quelques paramètres comme
OS_USERID,OS_GROUPIDetOS_USERNAME. Nous créons aussi quelques dossiers personnalisés pour cet utilisateur - Nous configurons notre shell pour des choses comme le fichier d'historique, pour avoir un prompt personnalisé, ...
- Nous nous assurons que les différents dossiers que nous devons manipuler appartiennent bien à notre utilisateur
- 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 :
- Créer l'image locale
- 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
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.
debugpy
Ce fichier contient simplement le nom 'debugpy', qui est le nom d'un paquet Python utilisé pour le débogage à distance et l'intégration avec des environnements de développement. C'est probablement un fichier de configuration ou de métadonnées qui identifie le paquet.
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
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éé.
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:Ce fichier Docker Compose configure un conteneur de développement nommé my_sample_devcontainer_app qui construit une image à partir du Dockerfile dans ./.devcontainer. Le conteneur monte le répertoire courant comme volume, partage le port 5679 pour le débogage, charge les variables d'environnement depuis deux fichiers .env, et reste actif indéfiniment grâce à la commande sleep infinity.
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 utilisonsjohndoeavec1001pour 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'utilisateur1000:1000localement, 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:5679pour 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. Cevscode-extensionssert à la persistance des données. - Et la ligne pour
./.devcontainer/history/.bash_historyconservera 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
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.
{ "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" ] } }}Ce fichier configure un environnement de développement containerisé pour VS Code en utilisant Docker Compose. Il définit le service à lancer, l'espace de travail, les variables d'environnement pour SSH, et personnalise l'éditeur avec des paramètres spécifiques et des extensions.
- Nous devons utiliser l'entrée
dockerComposeFilepour charger nos deux fichierscompose.yaml, celui de la racine (celui de prod) et celui du dossier.devcontainer. C'est nécessaire parce que nous voulons juste étendre lecompose.yamlde production. Nous réutilisons tous les services et clés définis dans../compose.yamlet en ajoutons (extension) ou en modifions (override) simplement quelques-uns - Nous devons indiquer à VSCode quel dossier doit être ouvert dans notre session Devcontainer, ce doit être
/app(doit correspondre à la variableAPP_HOMEde.devcontainer/compose.yaml) - Nous définissons le nom de notre remoteUser, ce sera
vscode(doit correspondre à la variableOS_USERNAMEde.devcontainer/compose.yaml) - Nous ajoutons aussi une variable d'environnement
GIT_SSH_COMMAND. C'est optionnel mais cela nous permettra d'exécuter une commandegit(commegit pull) depuis l'intérieur de notre devcontainer - La clé
initializeCommandcréera le dossier.devcontainer/historysur 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éevolumesdans.devcontainer/compose.yaml) - 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
À ce stade, notre projet ressemble à ceci, soit dix fichiers.
docker-prod-devcontainer/
Création de l'image Devcontainer
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 :
Vous obtiendrez quelque chose comme ceci :
name: my_sampleservices: 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: truenetworks: default: name: my_sample_defaultvolumes: vscode-extensions: name: my_sample_vscode-extensionsCe fichier Docker Compose définit un service de conteneur de développement appelé app. Il construit une image Docker à partir d'un Dockerfile, configure des variables d'environnement et des arguments de build pour Python 3.13, expose deux ports réseau, et monte des volumes pour synchroniser le code source et les extensions VS Code entre la machine hôte et le conteneur.
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.

Ouvrir le container avec VSCode
Il est temps d'ouvrir le projet avec la fonctionnalité Devcontainer de VSCode.

Si vous regardez en bas à droite de votre écran, vous verrez probablement cette popup :

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.

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 :
Dans ce projet de démo, vous pouvez voir ce message ; une seule fois :

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
Si nous retournons à l'URL http://localhost:8000, nous obtenons toujours la réponse JSON (le container tourne donc bien).
{"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 :

Il vous suffit d'enregistrer la modification, de revenir à la fenêtre du navigateur et de rafraîchir la page :
{"message":"Hello, FastAPI - I'm running from the Devcontainer!"}
Déboguons
Dans ce projet de démo, nous avons ajouté la dépendance Debugpy.
Il nous suffit d'ajouter un fichier supplémentaire pour VSCode.
{ "version": "0.2.0", "configurations": [ { "name": "Python: FastAPI", "type": "debugpy", "request": "attach", "connect": { "host": "localhost", "port": 5679, }, "justMyCode": true, "pathMappings": [ { "localRoot": "${workspaceFolder}/src", "remoteRoot": "." } ] } ]}Ce fichier configure le débogueur Visual Studio Code pour se connecter à un processus Python FastAPI en cours d'exécution sur localhost:5679. Il mappe le dossier local src du projet vers la racine distante et limite le débogage au code utilisateur plutôt qu'aux bibliothèques.
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.
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.

Pour aller plus loin
L'exemple ci-dessus utilise Python ; exactement la même approche à deux images est utilisée, langage par langage, dans Docker - Python devcontainer et le développement PHP dans un devcontainer avec des outils de qualité de code préinstallés. Et si le devcontainer reste une notion abstraite pour vous, commencez par Docker - Explique-moi comme si j'avais cinq ans - À quoi sert Docker ?.
Cet article fait partie de la série Développer dans un devcontainer :
- Développement PHP dans un devcontainer avec des outils de qualité de code préinstallés
- Installer un environnement PHP Docker en quelques secondes
- Docker - Python devcontainer
- Docker - Installation facile de Python sous Windows
- Docker - Une installation de Python encore plus simple
- Une seule image Docker pour la production et les devcontainers - la méthode propre
- Améliorez vos présentations avec Quarto, reveal.js, Decktape, Docker et les DevContainers
Chargement des commentaires…





