How to Set Up a Dev Container for a Python AI Project
A new engineer should be running your project minutes after cloning it, not a day later. This guide builds a dev container for a Python AI project: a Dockerfile and a devcontainer.json that pin Python, install dependencies with uv, forward ports, preload editor extensions, read API keys from the host environment and optionally pass a GPU through.
You need Docker and VS Code with the Dev Containers extension (ms-vscode-remote.remote-containers). The format is an open spec at containers.dev, so Codespaces and the devcontainer CLI read the same files.
1. Pin Python and uv in a Dockerfile
Create .devcontainer/Dockerfile:
FROM mcr.microsoft.com/devcontainers/python:3.12-bookworm
COPY --from=ghcr.io/astral-sh/uv:<uv-version> /uv /uvx /bin/
The tag sets the Python version. Microsoft's Python dev container image has tags for 3.9 through 3.15 and ships git plus a non-root vscode user. The COPY --from line is the install method from the uv Docker guide, which recommends pinning a release over latest so a rebuild next month doesn't change your tooling.
2. Write devcontainer.json for your Python dev container
Create .devcontainer/devcontainer.json:
{
"name": "python-ai-project",
"build": { "dockerfile": "Dockerfile" },
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"containerEnv": {
"UV_PROJECT_ENVIRONMENT": "/home/vscode/.venv"
},
"remoteEnv": {
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
"OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}"
},
"postCreateCommand": "uv sync --locked --all-groups",
"forwardPorts": [8000, 8888],
"portsAttributes": {
"8000": { "label": "API", "onAutoForward": "notify" },
"8888": { "label": "Jupyter", "onAutoForward": "notify" }
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"charliermarsh.ruff",
"ms-toolsai.jupyter"
],
"settings": {
"python.defaultInterpreterPath": "/home/vscode/.venv/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true
}
}
}
}
}
- Virtualenv location.
UV_PROJECT_ENVIRONMENTkeeps the virtualenv in the container's own filesystem, not the bind-mounted repo. Bind mounts add disk overhead on macOS and Windows, and a virtualenv is thousands of small files. - Dependencies.
postCreateCommandruns once, when the container is created.--lockedmakes a staleuv.lockfail loudly instead of quietly resolving new versions. - Ports.
forwardPortsalways forwards these to your machine andportsAttributeslabels them. 8000 and 8888 stand in for your API server and Jupyter. - Editor.
customizations.vscodeinstalls the extensions inside the container and points the interpreter at the virtualenv. - Features. Each key in
featuresis a registry path and{}takes the defaults. This one adds the GitHub CLI without touching the Dockerfile.
The devcontainer.json reference lists every property.
3. Pass API keys in from the host environment
The remoteEnv block reads each key with ${localEnv:NAME}, so no secret lands in the repo or the image. Export the key, then launch VS Code from that shell:
export ANTHROPIC_API_KEY="<your-anthropic-key>"
code .
On Windows, set it as a user environment variable and restart VS Code, because a running client may not see new variables. Unset variables resolve to an empty string, and ${localEnv:NAME:default} supplies a fallback.
For many variables, "runArgs": ["--env-file", ".devcontainer/devcontainer.env"] reads them from a file instead. Add that file to .gitignore and create it before building, or Docker won't start the container.
4. Open it and check it works
Press F1, run Dev Containers: Reopen in Container, then in the terminal:
uv run python --version
uv run python -c "import os; print('key set:', bool(os.environ.get('ANTHROPIC_API_KEY')))"
uv run pytest -q
The first build is slow; later opens reuse it. After editing the Dockerfile or containerEnv, run Dev Containers: Rebuild Container.
5. Add GPU access for teammates who have one
Merge these keys into the file above:
{
"hostRequirements": { "gpu": "optional" },
"features": {
"ghcr.io/devcontainers/features/nvidia-cuda:1": { "installCudnn": true }
}
}
Per the nvidia-cuda feature README, "optional" uses a GPU when the host has one, while true fails on machines without one. The feature only installs CUDA libraries. The host needs the NVIDIA driver and the NVIDIA Container Toolkit, and on Linux you register the runtime with Docker:
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Run nvidia-smi inside the container to confirm the GPU is visible.
Gotchas
- Slow files on Mac and Windows. On Windows, keep the repo inside the WSL 2 filesystem, or use Dev Containers: Clone Repository in Container Volume, which the VS Code docs say performs better than a bind mount.
- No GPU on a Mac. Docker on macOS can't reach one, so those teammates need a hosted model API or a CPU-sized model.
- Mixed GPU teams. Avoid
"runArgs": ["--gpus=all"], which forces the flag on every machine. Put the GPU config in.devcontainer/gpu/devcontainer.jsonand VS Code asks which config to open. - Dependencies change after creation.
postCreateCommanddoesn't rerun, so after pulling a newuv.lock, runuv sync --locked --all-groups. - Stale interpreter after a rebuild. Run "Python: Clear Cache and Reload Window" from the Command Palette.
Commit .devcontainer/ and every engineer, including the one who inherits your system at handover, starts from the same Python, dependencies and tools you shipped with.