<!--
  canonical-url: https://www.avonture.be/fr/blog/docker-python-devcontainer-windows
  published:     2024-12-01
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# 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 [31mERROR - %ERROR_MESSAGE%[0m

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.
