# How to use uv with VS Code devcontainers


A devcontainer hands every teammate the same Python interpreter and the same locked dependencies. [uv](https://pydevtools.com/handbook/reference/uv.md) 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](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) inside the container, points VS Code at the in-container interpreter, and uses the [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) to keep team environments in sync.

> [!NOTE]
> Devcontainer support in VS Code requires the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) (`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:

```dockerfile {filename=".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`:

```json {filename=".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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md)).

`--all-groups` installs every group declared under `[dependency-groups]` in [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md), so dev tools like [pytest](https://pydevtools.com/handbook/reference/pytest.md) and [Ruff](https://pydevtools.com/handbook/reference/ruff.md) 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](https://pydevtools.com/handbook/how-to/how-to-configure-vs-code-for-a-uv-project.md).

## 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+P` → **Developer: 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 **Code** → **Codespaces** → **Create 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:

```dockerfile
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](https://pydevtools.com/handbook/how-to/how-to-fix-the-externally-managed-environment-error.md) 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

- [Dev Containers documentation](https://containers.dev/) describes the `devcontainer.json` schema in full.
- [GitHub Codespaces documentation](https://docs.github.com/en/codespaces) covers the cloud-hosted devcontainer flow.
- [uv Docker integration guide](https://docs.astral.sh/uv/guides/integration/docker/) lists every published uv image variant and recommended environment variables.
- [`va-h/devcontainers-features/uv`](https://github.com/va-h/devcontainers-features) adds uv to Microsoft's `mcr.microsoft.com/devcontainers/python` image as a feature, for teams already standardized on that base image.
- [How to configure VS Code for a uv project](https://pydevtools.com/handbook/how-to/how-to-configure-vs-code-for-a-uv-project.md) covers the equivalent setup without devcontainers.
- [How to use uv in a Dockerfile](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md) covers production image patterns (multi-stage builds, BuildKit cache mounts, `--frozen` semantics) that complement the development setup in this guide.
