Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve devcontainer experience, particularly when switching from pip to uv. #12197

Open
jtkiley opened this issue Mar 15, 2025 · 9 comments
Open
Labels
enhancement New feature or improvement to existing functionality

Comments

@jtkiley
Copy link

jtkiley commented Mar 15, 2025

Summary

I tried changing an existing devcontainer to use uv instead of pip, and I ran into the error mentioned in ##11599.

Initial devcontainer.json with only uv added:

{
	"name": "Example",
	"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm",
	"features": {
		"ghcr.io/devcontainers-extra/features/apt-get-packages:1": {},
		"ghcr.io/va-h/devcontainers-features/uv:1": {}
	},
	"postCreateCommand": "pip install -r .devcontainer/requirements.txt",
	"customizations": {
		"vscode": {
			"extensions": [
				"ms-toolsai.jupyter",
				"charliermarsh.ruff"
			]
		}
	}
}

requirements.txt:

chromadb==0.6.3
ipykernel==6.29.5
ipywidgets==8.1.5
lancedb==0.21.1
polars==1.24.0
sentence-transformers==3.4.1

When I change postCreateCommand to use uv like this "postCreateCommand": "uv pip install -r .devcontainer/requirements.txt", I get this:

Running the postCreateCommand from devcontainer.json...

[4474 ms] Start: Run in container: /bin/sh -c uv pip install -r .devcontainer/requirements.txt
error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment

When I then add --system to get "postCreateCommand": "uv pip install --system -r .devcontainer/requirements.txt", I get another error:

      Built pypika==0.48.9
Prepared 134 packages in 8.74s
error: Failed to install: websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (websockets==15.0.1)
  Caused by: failed to create directory `/usr/local/lib/python3.12/site-packages/websockets-15.0.1.dist-info`: Permission denied (os error 13)

Example

Ideally, a user could simply add a uv feature to a devcontainer, change the postCreateCommand to use uv pip instead of pip, and have it work.

More broadly, it would be nice to see worked examples of how uv works nicely with devcontainers. Among other things, I work with beginners (other PhD researchers), and I build a devcontainer for the course I teach (and use them extensively in my own projects, both academic and external). It would be nice to see uv work well throughout the lifecycle of a project:

  1. Creation. If I'm creating a new devcontainer (e.g., using VSCode "Dev Containers: Add Dev Container Configuration Files..."), how can I do so easily? I usually build the container, pip install the needed packages, let it solve, pip freeze, and use that output to pin only the versions of the key packages (intended to be a portability habit). Then I rebuild again and work.
  2. Adding uv to an existing project. Ideally, I could do what I described above, but what's the intended way to do this? I tend to think that it should drop in a replacement for pip and then let folks work from there to use the other features.
  3. Adding packages. How should we add new packages? (This seems covered in the normal docs.)
  4. Updating packages. Academic projects often last for years, so there are often package updates that would add something helpful. How can I update packages, test if anything is broken (more like ad hoc experimentation than pytest), and keep/rollback the update? I usually teach a pip freeze approach, but the docs make it look potentially much easier with uv.

I think it would be great if uv had a devcontainer feature that simply handled all of the venv, --target, and other differences for users. In a devcontainer, there's generally just one Python install that matters, and that's usr/local/bin/python, which I think is the root of a lot of the previous discussion about --user.

@jtkiley jtkiley added the enhancement New feature or improvement to existing functionality label Mar 15, 2025
@juanjcardona13
Copy link

Same for here!
What I can see is that the interpreter inside the venv disappears. I'm not sure if it's related to it being a symbolic link; I'm pretty unfamiliar with the topic, but that's what I noticed.
Thanks

@zanieb
Copy link
Member

zanieb commented Mar 16, 2025

@juanjcardona13 could you please provide more details? that sounds like a different thing?

@zanieb
Copy link
Member

zanieb commented Mar 16, 2025

First of all, we can definitely create an integration guide for this. Thanks for flagging some of the things you'd want to see there.

I gave your example a quick try, and "postCreateCommand": "uv venv && uv pip install anyio", works fine. Why not just create the virtual environment?

If you're opposed to using a virtual environment since it adds indirection, then you can change to the root user "remoteUser": "root", and the permission error is also resolved.

Generally, I'd recommend using our top-level interface, e.g., uv add, uv sync, and uv run. The workflows you're asking about are much simpler that way and management of the environment is abstracted away.

@jtkiley
Copy link
Author

jtkiley commented Mar 16, 2025

I think it would be great to have a resource to link for people. As availability allows, I'm happy to help draft a bit and/or test.

I edited that line to "postCreateCommand": "uv venv && uv pip install -r .devcontainer/requirements.txt", rebuilt the devcontainer, and that worked for the setup with some caveats.

  1. Hardlink warning. In the rebuild, I got the warning below.
[0/135] Installing wheels...                               warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
         If the cache and target directories are on different filesystems, hardlinking may not be supported.
         If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
Installed 135 packages in 12.89s
  1. Existing notebook. An existing notebook already has a kernel chosen, and it's usr/local/bin/python. In an existing notebook, I get the error below. This is tough, because that package is installed, but now it's in a venv. Also, this is a notebook that worked perfectly before adding uv to the container. Following the suggestion in the error message won't really help, because it'll just move the missing package issue down the line until the imports in the notebook fail. This is an issue with requiring venvs.
Running cells with 'Python 3.12.1' requires the ipykernel package.
Run the following command to install 'ipykernel' into the Python environment. 
Command: '/usr/local/bin/python -m pip install ipykernel -U --user --force-reinstall'
  1. First new notebook. When creating a first new notebook and attempting to run a code cell, the only recommended Python kernel is /usr/local/bin/python. The venv does appear in other environments.
  2. Subsequent new notebooks. When creating another new notebook after having set the venv as the environment for the previous new notebook, both the venv (listed first) and /usr/local/bin/python (second) are listed without either shown as recommended.
  3. Terminal. In the terminal (within VSCode and running in the devcontainer), which python is /usr/local/bin/python.
  4. .py files. If I create a .py file, and then click "Run" in VSCode, it attempts to run it with /usr/local/bin/python.

I think things like this are why some devcontainer users really dislike the required venv.
It's not clear that it adds value to have it in a devcontainer, and it creates these foreseeable problems. Many of us remember exactly these issues from running locally with things like conda. After switching to devcontainers (conda friction and issues being a big motivator, at least for me), we're used to never seeing these at all. You set it up, dial it in, and it just works, and it works everywhere. That I think is what the ideal target is: a straightforward configuration that just works, and with as much as possible included in the devcontainer feature, rather than being left for every downstream user to configure.

There's also the beginner/student case, which is more specific to some of what I do, but it also got some discussion in the other threads. I used to use conda, and I had to set aside an hour in a workshop to troubleshoot, and, in bad cases, troubleshoot during breaks or after hours. When I switched to devcontainers (and recommended using Codespaces), that dropped basically to zero.

Personally, I don't like the root idea. It runs against this VSCode documentation suggestion, and I'm also not sure I can fully identify and mitigate the security implications for me and for wherever a devcontainer might end up (other devs, students, clients).

I'm definitely interested in the top level interface for new projects and modifying projects where uv has been added. At the moment, I'm just more focused on the switching case. I suspect that I'd rather not spend a few years with both setups, rather than spending what I hope is a small amount of time migrating everything to uv once the problem space and procedure become clear.

Edit: Forgot that Github doesn't ignore newlines in markdown paragraphs.

@zanieb
Copy link
Member

zanieb commented Mar 16, 2025

For most of those problems, it just sounds like you need to put the virtual environment at the front of your PATH so it is used by default? https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment

It runs against this VSCode documentation suggestion, and I'm also not sure I can fully identify and mitigate the security implications for me and for wherever a devcontainer might end up (other devs, students, clients).

I actually don't see a recommendation not to use the root user there? It just explains how to do so. I agree it's nice not to use the root user, but.. you're also trying to write to global state which the author of the images have decided should require root permissions.

It looks like just using sudo in the invocation works for that image

"postCreateCommand": "sudo uv pip install anyio --system"

@zanieb
Copy link
Member

zanieb commented Mar 16, 2025

Here's a more complete example using a virtual environment

{
	"name": "Python 3",
	"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
	"features": {
		"ghcr.io/va-h/devcontainers-features/uv:1": {}
	},
	"postCreateCommand": {
		// Fix permissions on the volume mount; see https://github.com/microsoft/vscode-remote-release/issues/9931
		// Create a virtual environment
		// Install an example package (you would install a requirements file in practice)
		"setupEnvironment": "sudo chown -R $(whoami): ${containerWorkspaceFolder}/.venv && uv venv ${containerWorkspaceFolder}/.venv && uv pip install anyio"
	},
	"mounts": [
		// Mount the `.venv` as a volume to avoid collision with the one  from
		// the host machine's workspace.
		// See https://github.com/astral-sh/uv-docker-example/blob/main/run.sh#L11
		"target=${containerWorkspaceFolder}/.venv,type=volume"
	],
	"remoteEnv": {
		// Activate the virtual environment
		"PATH": "${containerWorkspaceFolder}/.venv/bin:${containerEnv:PATH}"
	},
	"customizations": {
		// Use the virtual environment interpreter by default
		"vscode": {
			"extensions": [
				"ms-python.python",
				"ms-python.vscode-pylance"
			],
			"settings": {
				"python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv/bin/python"
			}
		}
	}
}

@jtkiley
Copy link
Author

jtkiley commented Mar 16, 2025

(Started writing this before the most recent replies.)

I spotted another issue that seems to be a venv issue.

After I wrote the previous part up, I noticed that Onedrive was syncing a lot of data. And, I could see in the filenames that it looked like a lot of package internals. So, I created a second copy of the devcontainer configuration and the notebook/data I'm working with.

  • pip: 1.4MB
  • uv: 1.39GB

It appears that the stock Python devcontainer image uses caching to store a lot of the devcontainer packages, so none of that hits the workspace that is linked into the container from the host filesystem.

In contrast, the uv venv is all in the .venv/ folder in the workspace, which ends up getting synced in full. In hindsight that's not surprising, since there's nothing linking up uv with that caching.

I poked around and didn't quite see explicitly how the caching works, but it may use the vscode docker volume that's mounted across containers. The top of the rabbit hole I went down is the Python image source. There are some environment variables in the container that may be relevant, but I don't know enough about those internals to be sure.

@jtkiley
Copy link
Author

jtkiley commented Mar 16, 2025

It runs against this VSCode documentation suggestion, and I'm also not sure I can fully identify and mitigate the security implications for me and for wherever a devcontainer might end up (other devs, students, clients).

I actually don't see a recommendation not to use the root user there? It just explains how to do so. I agree it's nice not to use the root user, but.. you're also trying to write to global state which the author of the images have decided should require root permissions.

It looks like just using sudo in the invocation works for that image

"postCreateCommand": "sudo uv pip install anyio --system"

Here's a link to the section that says:

Running your application as a non-root user is recommended even in production (since it is more secure), so this is a good idea even if you're reusing an existing Dockerfile.

For now, I wanted to point that out, and I'll test out the other things you posted. Thanks again for all of your help.

@zanieb
Copy link
Member

zanieb commented Mar 16, 2025

I agree using a non-root user in production makes sense. I'm not sure if I'd go out of my way to do so in a development container. Regardless, we should support it.

It appears that the stock Python devcontainer image uses caching to store a lot of the devcontainer packages, so none of that hits the workspace that is linked into the container from the host filesystem.

Interesting... we'll need to look into that. I wonder if that's why we get a link-mode warning. I'm sure we can setup a shared uv cache.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improvement to existing functionality
Projects
None yet
Development

No branches or pull requests

3 participants