🧭 How toSoftwareIntermediate✨ AI-assisted

How to Set Up a Dev Container for a Python AI Project

WittyTech Daily··4 min read
#dev containers#python#vscode#uv

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_ENVIRONMENT keeps 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. postCreateCommand runs once, when the container is created. --locked makes a stale uv.lock fail loudly instead of quietly resolving new versions.
  • Ports. forwardPorts always forwards these to your machine and portsAttributes labels them. 8000 and 8888 stand in for your API server and Jupyter.
  • Editor. customizations.vscode installs the extensions inside the container and points the interpreter at the virtualenv.
  • Features. Each key in features is 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.json and VS Code asks which config to open.
  • Dependencies change after creation. postCreateCommand doesn't rerun, so after pulling a new uv.lock, run uv 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.

← More in Software