Skip to content

How to use uv with VS Code devcontainers

uv

A devcontainer hands every teammate the same Python interpreter and the same locked dependencies. uv makes that hand-off cheap: a single uv sync --locked reads uv.lock and installs the resolved graph in seconds, on every machine.

This guide configures a VS Code devcontainer that uses uv to materialize the project’s virtual environment inside the container, points VS Code at the in-container interpreter, and uses the lockfile to keep team environments in sync.

Note

Devcontainer support in VS Code requires the Dev Containers extension (ms-vscode-remote.remote-containers) and a working Docker installation. GitHub Codespaces uses the same devcontainer.json format with no Docker install on the local machine.

Add a devcontainer.json with the official uv image

Create .devcontainer/devcontainer.json and .devcontainer/Dockerfile at the repository root. The Dockerfile uses Astral’s prebuilt uv image so uv and the Python toolchain are already installed:

.devcontainer/Dockerfile
FROM ghcr.io/astral-sh/uv:python3.13-trixie-slim

ENV UV_LINK_MODE=copy \
    UV_COMPILE_BYTECODE=1 \
    UV_PROJECT_ENVIRONMENT=/workspaces/.venv

WORKDIR /workspaces

UV_LINK_MODE=copy silences cross-filesystem warnings between the cache and the project, UV_COMPILE_BYTECODE=1 precompiles .pyc files so the first import after a rebuild is not slow, and UV_PROJECT_ENVIRONMENT puts the venv at a stable absolute path that VS Code can reference.

The matching devcontainer.json:

.devcontainer/devcontainer.json
{
    "name": "uv-python",
    "build": { "dockerfile": "Dockerfile" },
    "postCreateCommand": "uv sync --locked --all-groups",
    "customizations": {
        "vscode": {
            "extensions": [
                "ms-python.python",
                "charliermarsh.ruff"
            ],
            "settings": {
                "python.defaultInterpreterPath": "/workspaces/.venv/bin/python",
                "python.terminal.activateEnvironment": false
            }
        }
    }
}

postCreateCommand runs once after the container is built. The flag choice matters more than it looks:

  • uv sync --locked re-resolves pyproject.toml and fails if the lockfile would change. Right for a team devcontainer: if a teammate forgot to commit a regenerated lockfile, the rebuild fails loudly instead of silently producing a different environment.
  • uv sync --frozen installs whatever the lockfile contains without checking it against pyproject.toml. Faster (no resolution step), but a stale lockfile installs silently. Right for production Docker images (see How to use uv in a Dockerfile).

--all-groups installs every group declared under [dependency-groups] in pyproject.toml, so dev tools like pytest and Ruff land in the container alongside runtime deps.

python.terminal.activateEnvironment is set to false because uv manages activation through uv run. If the Python extension also tries to activate the venv, every new terminal prints a benign but noisy activation script. For a deeper walk-through of the Python and Ruff extensions, see How to configure VS Code for a uv project.

Point VS Code at the in-container venv

The Python extension auto-discovers .venv/ at the workspace root, but a devcontainer mounts the workspace at /workspaces/<project-name> and the venv lives at /workspaces/.venv (the path that UV_PROJECT_ENVIRONMENT sets in the Dockerfile). Setting python.defaultInterpreterPath to that absolute path tells the extension where to look on first launch.

Tip

If VS Code shows “Python interpreter not found” after the container starts, run uv sync --locked in the integrated terminal and reload the window with Cmd+Shift+PDeveloper: Reload Window. The interpreter does not exist until uv sync has populated /workspaces/.venv.

For projects that use multiple Python versions or workspace members, override the interpreter per-folder in .vscode/settings.json instead of in devcontainer.json so each member can pin its own venv.

Run the same setup in GitHub Codespaces

GitHub Codespaces reads the same .devcontainer/ directory. Push the branch, click CodeCodespacesCreate codespace on main in the GitHub UI, and Codespaces builds the container in the cloud and connects a browser-based VS Code to it. No local Docker required.

Codespaces enforces two extra constraints worth knowing:

  • The base image must be available to GitHub’s runners. Public images on ghcr.io, mcr.microsoft.com, and Docker Hub work without configuration.
  • Post-create commands run with a 60-minute timeout. uv sync --locked finishes in a few seconds on most projects, so this is rarely a concern.

Pin the uv version

ghcr.io/astral-sh/uv:python3.13-trixie-slim floats to the latest uv release. For reproducible team setups, pin to a specific uv version:

FROM ghcr.io/astral-sh/uv:0.11.11-python3.13-trixie-slim

This guarantees every teammate gets the same uv binary, not just the same Python version. Bump the pin in a single commit when a new uv release ships features the team wants.

Troubleshooting

uv sync fails with “no project found”: The WORKDIR in the Dockerfile must match where the workspace is mounted, or postCreateCommand runs in the wrong directory. Set WORKDIR /workspaces in the Dockerfile, since VS Code mounts the host workspace under /workspaces/<project-name>.

VS Code uses the wrong interpreter after rebuild: Open the Command Palette and run Python: Clear Cache and Reload Window. Stale interpreter cache survives container rebuilds in some VS Code versions.

“externally-managed-environment” error from uv pip install: A devcontainer running uv pip install outside a venv hits Debian’s externally-managed-environment protection. Use uv sync or uv add against the project’s venv instead.

Permission errors writing to /workspaces/.venv: When mixing the official uv image (root by default) with a Microsoft Python image’s vscode user, the venv ends up owned by the wrong user. Either stay on one image family or set "remoteUser": "root" in devcontainer.json for development containers where root is acceptable.

Learn More

Last updated on