Docker - Python devcontainer
Learn how to quickly set up a VSCode devcontainer for Python development using Docker. Get the full Dockerfile, compose.yaml, and .docker.env configurations.
This article is over a year old but was reviewed on July 30, 2026 — the content is still accurate.
This article is part of the Coding using a devcontainer series:
- PHP development in a devcontainer with preinstalled code quality tools
- Install a PHP Docker environment in a matter of seconds
- Docker - Python devcontainer
- Docker - Easy setup of Python under Windows
- Docker - Even easier setup of Python
- One Docker Image for Production and Devcontainers - The Clean Way
- Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers

This article builds a reusable Python devcontainer for VSCode: a Dockerfile, compose.yaml, .docker.env (for per-project settings like the Python version), and a makefile wrapping common commands (make up, make bash), then a .devcontainer/devcontainer.json preconfigured with the Python extension and settings so make devcontainer opens VSCode running fully inside the container with files synced to the host.
As you know, VSCode is a superb editor that lets you program in probably any programming language. An editor, not an IDE, because VSCode is basically a Notepad in its ultimate version.
If you want to program in Python, you'll need to install a few extensions in VSCode to be really comfortable, i.e. syntax highlighting, code navigation, code refactoring (like renaming a variable or a class), etc.
There are ‘ready-to-use’ editors like PyCharm but 1. they cost money and 2. they are specific (you won't be able to program in PHP with PyCharm; or even work easily with HTML/CSS files).
In this new article, we'll look at how to get a VSCode environment ready to use straight away for coding in Python, and as it's VSCode it's 1. free, 2. multi-purpose and 3. insanely powerful.
Let's see it running
Here's what you get, before you create a single file: run make up in your console and you'll get this screen:

As you can see, we've a lot of commands like make up to start our Docker container. Let's try, and ouch, we're missing a file called /src/requirements.txt.

src/requirements.txt
Please create an empty file called src/requirements.txt. The file can stay empty (you'll add your own requirements later on).
By running make up again, yes!, this time we can build our images and create our container:

This time we can, if you need to, enter our container by running make bash and, for instance, check what's inside our current folder and which version of Python has been installed:

Note
As you see, Python 3.13 is used. Why that specific version? Just go back to your .docker.env file and take a look at the DOCKER_PYTHON_VERSION variable. If you need another one, just update the .docker.env file and run make up again.
Let's start creating our first script
If you're still inside your container, please type exit so your prompt will be the one of your machine (your host), no longer the one of your running container.
Please create your first script: create a new file in the src folder and call that file hello.py.
Be creative and type a simple print statement:
print("I'm your Python code")

To be able to run the code, start make bash again (to jump in the container) and start python hello.py

Information
It will work because, in the container, the working directory is /app/src. If this had not been the case, we would have had to write, for example, python /app/src/hello.py i.e. the absolute path to the script.
As you can see, files on your machine are synchronized with your host. If VSCode is still open, you can change your script to f.i.
print("Hey! It's synchronized; cool!")

The files behind all this — Dockerfile, compose.yaml, .docker.env and the makefile — are shown next.
Let's create the files for our Docker environment
In this article, we'll create the devcontainer i.e. the development environment based on Docker that you just saw running. The creation of the devcontainer will take a few minutes but it's just a question of copy/paste from this article to your system.
Once the files have been created, you can reuse the devcontainer for all your Python projects.
Two natural extensions: add the tools from Python - Code Quality tools to the image so every project inherits them, and read One Docker Image for Production and Devcontainers - The Clean Way to keep that tooling out of the image you deploy.
Like always, we'll create a new folder and create files there. Please run mkdir /tmp/python && cd $_ to create that folder and jump in it.
Start VSCode from there i.e. run code . in the console when you're located in the folder. This will open VSCode and you'll be able to create as many files you want.
Dockerfile
The first file to create will be used to build our Docker image. Please create a file called Dockerfile with the following content:
# 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 fileARG DOCKER_APP_HOME="/app"ARG DOCKER_CONTAINER_NAME="python"ARG DOCKER_OS_GROUPID=1000ARG DOCKER_OS_USERID=1000ARG 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_HOMEWORKDIR "${DOCKER_APP_HOME}/src"
# hadolint ignore=DL3008RUN --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 runningENTRYPOINT ["tail", "-f", "/dev/null"]
# endregion
# region - Define our development imageFROM base AS development
ENV SHELL /bin/bash
# Our user will be part of the root group since we're building the development imageARG DOCKER_OS_GROUPIDARG DOCKER_OS_USERIDARG 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"
# endregionThis Dockerfile creates a Python development container with two stages: a base image that installs Python, Linux tools, and Python dependencies with smart caching, and a development image that extends the base with a non-root user account, VS Code server directories, and bash customization for persistence. The goal is to provide a reproducible, efficient development environment where the user can work with proper isolation and maintain command history across container restarts.
compose.yaml
Next to the Dockerfile, we'll create our compose.yaml one. Please create that file with the following content:
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:This Docker Compose configuration sets up a Python development container named app_python. It builds from a Dockerfile in the current directory using the 'development' stage, passes in several customizable environment variables for Python version and container user settings, and mounts the local codebase plus two named volumes to persist VSCode extensions and bash history across container restarts.
.docker.env
The third file to create will be called .docker.env where we'll initialize some values. Please create that file with the content below:
# Application root directory in the container (PHP or NGINX) (--app-home)DOCKER_APP_HOME=/app
# Name of the container to show in the bash promptDOCKER_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 useDOCKER_PYTHON_VERSION=3.13-slimThis configuration file sets up environment variables for a Docker container running a Python application. It defines the application directory path, container identification, Linux user/group IDs for permission management, and the Python version to use.
Information
All you have to do is duplicate the other files we've created for each of your projects and the settings for your project will be made here, in the .docker.env file.
makefile
To make life easier, we're going to group together a set of commands in a file called makefile. Please create a makefile file with the contents below:
# cspell:ignore ifdef,pydocstyle,isort,mypydefault: help
# Folder where Python binaries are installedBIN=/usr/local/bin
# Name of our Docker containerCONTAINER=app_python
# The project typeDOCKER_PROJECT_TYPE=Python
# Name of our Docker environment fileDOCKER_ENV_FILE=.docker.env
# The environment file we'll useENV=--env-file ${DOCKER_ENV_FILE}
# The list of Docker compose yaml file we'll useYAML=COMPOSE_FILE=compose.yaml
COLOR_CYAN:=36_CYAN := "\033[1;${COLOR_CYAN}m%s\033[0m %s\n"
# Required for `make devcontainer`: we need to convert a string like "python" to# an hexadecimal string. (printf "python" | xxd -p will return 63325f66646d5f706870)DOCKER_VSCODE:=$(shell printf "${CONTAINER}" | xxd -p)
.PHONY: bashbash: ## App - Start an interactive shell inside your application container ${YAML} docker compose ${ENV} exec -it ${CONTAINER} /bin/bash
.PHONY: buildbuild: ## App - Build a fresh Docker image for this project ${YAML} docker compose ${ENV} build ${ARGS}
.PHONY: configconfig: ## App - Show the docker configuration ${YAML} docker compose ${ENV} config
.PHONY: devcontainerdevcontainer: ## App - Open the PHP container in vscode (you can f.i. directly start your debugging session) code --folder-uri vscode-remote://attached-container+${DOCKER_VSCODE}/app
.PHONY: helphelp: ## App - Show the help with the list of commands @clear
# Parse this file, search for `##` followed by a description @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[0;33m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
@echo ""
.PHONY: removeremove: ## App - Stop containers and remove them (you will lose updated files if you do not have locally) ${YAML} docker compose ${ENV} down --remove-orphans --rmi all --volumes
.PHONY: upup: ## App - Based on the created Docker images (made with `make build`), create containers and start them ${YAML} docker compose ${ENV} up --detach
##@ Quality assurance
.PHONY: doc-styledoc-style: ## QA - pydocstyle is a static analysis tool for checking compliance with Python docstring conventions @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pydocstyle /app/src
.PHONY: formatformat: ## QA - Format the script using Black @clear @${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/black /app/src
.PHONY: lintlint: ## QA - Lint the script using Pylint - Run analyses your code without actually running it @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pylint /app/src
.PHONY: mypymypy: ## QA - Mypy is a program that will type check your Python code @clear ${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/mypy --cache-dir /tmp/mypyThis Makefile defines a set of commands for managing a Python application running in Docker. It sets up variables for Docker container configuration, environment files, and binaries, then provides convenient `make` commands for common tasks like building and running the container, opening shells, and running quality assurance tools like linting and type checking.
Caution
If you don't know if you already have GNU make, just run which make in the console. If you see make not found then please run sudo apt-get update && sudo apt-get install make to proceed with the installation.
What have we done so far?
We have created the minimal structure for our future Python projects. We've defined our Docker image (thanks to the Dockerfile file) and how to use it (compose.yaml file).
We've also configured our project (.docker.env file) and set up a number of useful commands (makefile file).
All these files can be used across all your Python projects.
Now, let's create our devcontainer environment
We have to create an additional file. Please create a new folder called .devcontainer and, there, a file called devcontainer.json. Copy/paste the content below:
{ "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 } } }}This is a VS Code development container configuration file that sets up a Python development environment running in Docker. It specifies which Docker container to use, installs a curated list of VS Code extensions for Python development, and configures editor settings like code formatting, linting, indentation, and language-specific behaviors to enforce consistent code style across the project.
This file is very long; it can be shorter, but the idea is to configure VSCode, so we've specified all the extensions we want in our coding environment, along with a bunch of settings.
We've finished our set-up. Here is our final project's structure:

Start the project in a devcontainer
If VSCode is still open, please close it. If you're still in the container's console, please type exit to go back to your host console.
Now, please run make devcontainer (on your host machine thus), which is one of the commands we already have in our makefile.
VSCode will start and open the project directly inside the container:

VSCode will ask (see bottom right) to install the Microsoft Python extension; just allow this and click on Install.
VSCode will also ask if you want to install recommended extensions; please do it.

Conclusion
You now have a fully working coding environment for Python. Thanks to our Docker image, Python has been installed and configured to run in a Docker container (understand: nothing was installed on your machine) and thanks to the Devcontainer, you're sure that VSCode is properly configured with all required extensions to work with ease.
Running Windows and want the exact same files adapted for it? See Docker - Easy setup of Python under Windows. Prefer to skip creating files by hand entirely? Docker - Even easier setup of Python uses VSCode's built-in wizard instead.
This article is part of the Coding using a devcontainer series:
- PHP development in a devcontainer with preinstalled code quality tools
- Install a PHP Docker environment in a matter of seconds
- Docker - Python devcontainer
- Docker - Easy setup of Python under Windows
- Docker - Even easier setup of Python
- One Docker Image for Production and Devcontainers - The Clean Way
- Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers





