Skip to main content

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.

· 7 min read
View raw
Christophe
Markdown, WSL and Docker lover ~ Insatiable curious.
Cet article existe aussi en français — lire la version française.

This article is over a year old but was reviewed on July 30, 2026 — the content is still accurate.

Docker - Python devcontainer

⚡
TL;DR

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:

Makefile

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.

requirements.txt is missing

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:

Docker up

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:

Inside the container

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

Hello

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

Run hello

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!")

It's synchronized

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

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

Your values for this page
default
Applies to the commands on this pageSaved on this device only
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 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

This 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,mypy
default: help
# Folder where Python binaries are installed
BIN=/usr/local/bin
# Name of our Docker container
CONTAINER=app_python
# The project type
DOCKER_PROJECT_TYPE=Python
# Name of our Docker environment file
DOCKER_ENV_FILE=.docker.env
# The environment file we'll use
ENV=--env-file ${DOCKER_ENV_FILE}
# The list of Docker compose yaml file we'll use
YAML=COMPOSE_FILE=compose.yaml
COLOR_CYAN:=36
_CYAN := "\033[1;${COLOR_CYAN}m%s\033[0m %s\n"
# Required for `make devcontainer`: we need to convert a string like "python" to
# an hexadecimal string. (printf "python" | xxd -p will return 63325f66646d5f706870)
DOCKER_VSCODE:=$(shell printf "${CONTAINER}" | xxd -p)
.PHONY: bash
bash: ## App - Start an interactive shell inside your application container
${YAML} docker compose ${ENV} exec -it ${CONTAINER} /bin/bash
.PHONY: build
build: ## App - Build a fresh Docker image for this project
${YAML} docker compose ${ENV} build ${ARGS}
.PHONY: config
config: ## App - Show the docker configuration
${YAML} docker compose ${ENV} config
.PHONY: devcontainer
devcontainer: ## App - Open the PHP container in vscode (you can f.i. directly start your debugging session)
code --folder-uri vscode-remote://attached-container+${DOCKER_VSCODE}/app
.PHONY: help
help: ## App - Show the help with the list of commands
@clear
# Parse this file, search for `##` followed by a description
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<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: remove
remove: ## App - Stop containers and remove them (you will lose updated files if you do not have locally)
${YAML} docker compose ${ENV} down --remove-orphans --rmi all --volumes
.PHONY: up
up: ## App - Based on the created Docker images (made with `make build`), create containers and start them
${YAML} docker compose ${ENV} up --detach
##@ Quality assurance
.PHONY: doc-style
doc-style: ## QA - pydocstyle is a static analysis tool for checking compliance with Python docstring conventions
@clear
${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pydocstyle /app/src
.PHONY: format
format: ## QA - Format the script using Black
@clear
@${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/black /app/src
.PHONY: lint
lint: ## QA - Lint the script using Pylint - Run analyses your code without actually running it
@clear
${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/pylint /app/src
.PHONY: mypy
mypy: ## QA - Mypy is a program that will type check your Python code
@clear
${YAML} docker compose ${ENV} exec ${CONTAINER} ${BIN}/mypy --cache-dir /tmp/mypy

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

Our final project&#39;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 - Devcontainer

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.

Install recommended extensions

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.


Was this article helpful?
Did you try the steps in this article?