Aller au contenu principal

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

· 20 minutes de lecture
Voir la source
Christophe
Amoureux de Markdown, WSL et Docker ~ Curieux insatiable.
Cet article existe aussi en anglais — lire la version originale.

Traduction automatique

Cette traduction a été produite par une IA. Elle peut contenir des contresens ou des maladresses, en particulier sur les termes techniques. En cas de doute, consultez l'article de référence en anglais — c'est lui qui fait foi.

Cet article a plus d'un an. Les informations peuvent être obsolètes.

Une seule image Docker pour la production et les devcontainers - la méthode propre

⚡
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 ?​

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.

Concept clé - Deux images

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.

Vos valeurs pour cette page
défaut
S'applique aux commandes de cette pageEnregistré uniquement sur cet appareil

Voici le container de production qui répond sur http://localhost:8000 :

http://localhost:8000

{"message":"Hello, FastAPI - PRODUCTION!"}

L'image de production a été créée

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.

>user@machine: ~/yourproject

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.13
TIMEZONE="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.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"]

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.txt dans 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:1000 ou 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:true

Ce 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

.env
Dockerfile
compose.yaml
requirements.txt
▼src
main.py

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é.

>user@machine: ~/yourproject

Vous obtiendrez quelque chose comme ceci :

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

Ce 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 :

>user@machine: ~/yourproject

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 :

>user@machine: ~/yourproject

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 :

http://localhost:8000

{"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 :

>user@machine: ~/yourproject

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

É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=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 <<EOF >> "$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}"

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_GROUPID et OS_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 :

  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​

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 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​

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 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
  • 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)
  • Nous définissons le nom de notre remoteUser, ce sera vscode (doit correspondre à la variable OS_USERNAME de .devcontainer/compose.yaml)
  • 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
  • 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)
  • 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/

.devcontainer
.env
Dockerfile
compose.yaml
devcontainer.json
requirements.txt
.env
Dockerfile
compose.yaml
requirements.txt
▼src
main.py

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 :

>user@machine: ~/yourproject

Vous obtiendrez quelque chose comme ceci :

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

Ce 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.

Les deux images, prod et devcontainer, ont été créées

Ouvrir le container avec VSCode​

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

>user@machine: ~/yourproject

Ouverture de VSCode

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

La popup devcontainer

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

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&#39;un nouveau terminal

Dans ce projet de démo, vous pouvez voir ce message ; une seule fois :

Le message de bienvenue

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).

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

Il vous suffit d'enregistrer la modification, de revenir à la fenêtre du navigateur et de rafraîchir la page :

http://localhost:8000

{"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.

Ajout d&#39;un point d&#39;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.

Réussi

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 ?.


🦋 Rejoignez la conversation sur BlueskyUn compte Bluesky gratuit est nécessaire pour aimer, repartager ou commenter. Ce bloc interroge l'API publique de Bluesky directement depuis votre navigateur (sans cookie, sans connexion) — votre adresse IP parvient donc aux serveurs de Bluesky dès que cette section se charge.

Chargement des commentaires…

Cet article vous a-t-il été utile ?
Avez-vous suivi les étapes de cet article ?