# Fix uv permission denied errors in non-root Docker containers

> Separate system Python, virtual environment, cache, and managed Python permissions. Build dependencies once, then run your container as an unprivileged user.

Canonical URL: https://www.devobs.io/articles/uv-permission-denied-non-root-docker/
By: Claire Dubois
Published: 2026-10-14T19:22:28.091Z
Updated: 2026-10-14T19:22:28.091Z
Section: Architecture

A `Permission denied` error under `/usr/local/lib/python3.12/site-packages` usually means uv is trying to install into a Python environment that the current container user cannot modify. Setting `UV_SYSTEM_PYTHON=1` changes environment selection; it does not grant filesystem permissions. For an application image, install dependencies during the build and run the finished environment as an unprivileged user.

This distinction matters when migrating from pip. uv does not implement pip's `--user` installation scheme or its fallback to a user installation when the target directory is unwritable. Its documentation recommends virtual environments instead. Adding `--user` to the failing command is therefore not the fix. [uv's compatibility guide](https://docs.astral.sh/uv/pip/compatibility/#user-and-the-user-install-scheme)

## Find the path that failed

Start with the complete error, including the destination path. A cache failure and a package installation failure need different changes:

| Failing destination | What to check | Appropriate change |
|---|---|---|
| `/usr/local/lib/python…/site-packages` | System Python selected after `USER` changed | Install at build time or target a writable virtual environment |
| `/opt/venv/lib/python…/site-packages` | Existing environment ownership | Rebuild dependencies, or explicitly provide a user-owned environment for a development workflow |
| A directory beneath the uv cache | Cache location and its owner | Choose a writable `UV_CACHE_DIR`; keep the installation target separate |
| A managed Python installation directory | Interpreter download location | Install Python during the build, or configure a writable `UV_PYTHON_INSTALL_DIR` |

The last two variables address separate storage locations. Neither makes `site-packages` writable. `UV_PYTHON_DOWNLOADS=never` prevents an automatic Python download when the image already provides the intended interpreter. These controls are documented in uv's [environment variable reference](https://docs.astral.sh/uv/reference/environment/).

Inspect the actual process identity and interpreter inside the failing container:

```sh
id
python -c 'import sys, sysconfig; print(sys.executable); print(sys.prefix); print(sysconfig.get_path("purelib"))'
uv cache dir
```

Compare that interpreter with the one passed to uv. An explicit `uv pip install --python /opt/venv/bin/python …` removes ambiguity. `--system` deliberately bypasses virtual environments, so adding it to a command intended for `/opt/venv` moves in the wrong direction. [uv environment selection](https://docs.astral.sh/uv/pip/environments/)

## Build dependencies before switching users

Here is a small, complete example using the pip-compatible interface. It pins uv, Python, and a dependency so the example has a clear version boundary. These are reproducible example versions, not a claim that they are the newest releases. Select maintained versions for your deployment and rerun the checks when updating them.

Save this as `Dockerfile`:

```dockerfile
FROM python:3.12.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:0.9.21 /uv /usr/local/bin/uv

ENV UV_PYTHON_DOWNLOADS=never \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/opt/venv/bin:$PATH"

RUN uv venv --python /usr/local/bin/python /opt/venv \
    && uv pip install --no-cache --python /opt/venv/bin/python idna==3.10

RUN groupadd --gid 10001 app \
    && useradd --uid 10001 --gid app --create-home app

WORKDIR /app
COPY app.py /app/app.py
USER 10001:10001
CMD ["/opt/venv/bin/python", "/app/app.py"]
```

Save the following as `app.py`:

```python
import os
import sys
import idna

assert os.getuid() == 10001
assert sys.prefix == "/opt/venv"
assert not os.access("/opt/venv/lib/python3.12/site-packages", os.W_OK)
print(f"uid={os.getuid()} prefix={sys.prefix} idna={idna.__version__}")
print(idna.encode("münich.example").decode())
```

The environment belongs to root because the installation runs before `USER`. The application can read and execute its dependencies without being able to replace them. Docker's `USER` instruction changes subsequent `RUN` commands and the runtime identity; moving it above the dependency installation changes who needs write access. [Dockerfile USER reference](https://docs.docker.com/reference/dockerfile/#user)

The absolute interpreter in `CMD` also makes startup independent of shell activation. There is no installation command on the startup path. uv's [Docker guide](https://docs.astral.sh/uv/guides/integration/docker/) documents copying its binary from an image and running from a virtual environment. For stricter build reproducibility, replace mutable image tags with verified image digests.

## Verify both execution and permissions

Build and run the example:

```sh
docker build -t uv-permissions-demo .
docker run --rm --network=none --read-only uv-permissions-demo
```

This example was built and run on Linux ARM64 with uv 0.9.21 and Python 3.12.12. Other architectures were not tested. The expected output is:

```text
uid=10001 prefix=/opt/venv idna=3.10
xn--mnich-kva.example
```

The assertions check three independent properties: the process is unprivileged, it uses the intended environment, and its package directory is not writable. Running without a network and with a read-only root filesystem also checks that this tiny application's startup does not depend on downloading or modifying packages. A real application may need an explicitly writable directory for uploads, state, or temporary files; test those requirements separately.

To exercise the failure boundary, run a disposable container with the system installation command:

```sh
docker run --rm uv-permissions-demo \
  uv pip install --system --no-cache idna==3.10
```

This command requires package-index access and should fail while writing to system `site-packages`. It does not target the already populated virtual environment. The error confirms why changing the Python selection alone cannot solve the permission problem.

## When installation really must happen as non-root

Development containers sometimes need dependency changes after startup. In that case, deliberately create an environment owned by the development user. For example, create `/home/app/venv` while running as `app`, then install with `--python /home/app/venv/bin/python`. Ensure the home directory and cache are writable too.

Avoid applying recursive ownership changes to `/usr/local` just to make an installation pass. Give the specific environment the ownership required by its workflow. A read-only root filesystem additionally needs an appropriately owned writable mount for any environment that will change at runtime; changing ownership inside the image cannot make a read-only mount writable.

For an existing uv project, use its committed lockfile and install during the build with `uv sync --locked --no-dev`. Start the installed application's interpreter or entry point directly. `uv run` normally checks and synchronizes the environment before execution; using it at startup can introduce writes that an immutable application image was not designed to permit. [Locking and syncing](https://docs.astral.sh/uv/concepts/projects/sync/)

The original [non-root installation report](https://github.com/astral-sh/uv/issues/13107) concerned uv 0.6.16 and Python 3.12.10 in 2025. It is useful evidence of the migration trap, not proof of a new uv regression. Diagnose today's failure by its path and process identity: the user running an installation needs write access to that installation's destination, while the user running the finished application usually needs only read and execute access.

## Source references

- <https://docs.astral.sh/uv/pip/compatibility/#user-and-the-user-install-scheme>
- <https://docs.astral.sh/uv/reference/environment/>
- <https://docs.astral.sh/uv/pip/environments/>
- <https://docs.docker.com/reference/dockerfile/#user>
- <https://docs.astral.sh/uv/guides/integration/docker/>
- <https://docs.astral.sh/uv/concepts/projects/sync/>
- <https://github.com/astral-sh/uv/issues/13107>
