Files
prompts/docs/skills/fastapi-uv-docker/references/uv-project-layout.md
T
John Lancaster 3347443ca9 formatting
2026-06-19 01:29:05 -05:00

5.8 KiB

uv Project Layout and Dependency Management

!!! info "Primary sources" - uv project guide - uv project layout - uv Docker integration


Core Files

File Purpose Commit?
pyproject.toml Project metadata, deps, tool config Yes
uv.lock Exact resolved versions, cross-platform Yes
.python-version Default Python version for the project Yes
.venv/ Local virtual environment No (.gitignore)

!!! warning "Commit the lockfile" uv.lock must be committed. It is the source of truth for reproducible installs in CI and Docker. Never edit it by hand.


Essential Commands

# Initialize a new project (app, not library)
uv init --app

# Initialize a library (installable package with src layout)
uv init --lib

# Add a runtime dependency
uv add fastapi[standard]

# Add multiple dependencies at once
uv add uvicorn[standard] pydantic-settings

# Add dev-only dependency
uv add --dev pytest httpx pytest-asyncio ruff mypy

# Remove a dependency
uv remove requests

# Upgrade a specific package (keeps rest of lockfile intact)
uv lock --upgrade-package fastapi

# Upgrade all packages
uv lock --upgrade

# Sync env to lockfile (install/remove as needed)
uv sync

# Sync without dev deps (e.g., in CI or Docker)
uv sync --no-dev

# Sync and assert lockfile is up-to-date (for CI / Docker)
uv sync --locked

# Run a command in the project environment
uv run pytest
uv run uvicorn my_app.main:app --reload

# Run a one-off command without installing anything permanently
uv run --with httpx python -c "import httpx; print(httpx.__version__)"

pyproject.toml Reference

[project]
name = "my-app"
version = "0.1.0"
description = "Production FastAPI service"
requires-python = ">=3.12"
license = { text = "MIT" }
readme = "README.md"

dependencies = [
    "fastapi[standard]>=0.115",
    "uvicorn[standard]>=0.34",
    "pydantic-settings>=2.0",
]

[project.scripts]
# Creates `my-app` CLI entry point when installed
my-app = "my_app.main:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/my_app"]

[tool.uv]
dev-dependencies = [
    "pytest>=8",
    "pytest-asyncio>=0.24",
    "httpx>=0.27",     # needed for FastAPI TestClient
    "ruff>=0.6",
    "mypy>=1.11",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
strict-markers = true

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "S"]

[tool.mypy]
strict = true

src Layout with uv

Use uv init --lib or set up manually:

my-app/
├── pyproject.toml
├── uv.lock
├── .python-version    # e.g., "3.12"
├── .env.example
├── README.md
├── src/
│   └── my_app/
│       └── __init__.py
└── tests/
    └── conftest.py

The src/ layout prevents the local directory from shadowing the installed package, which would otherwise cause silent test failures when testing the installed version.

hatchling config for src layout:

[tool.hatch.build.targets.wheel]
packages = ["src/my_app"]

Migration from pip / requirements.txt

# 1. Initialize uv in an existing project
uv init --no-workspace   # if already has pyproject.toml, skip this

# 2. Import from requirements.txt
uv add -r requirements.txt

# 3. Import dev requirements
uv add --dev -r requirements-dev.txt

# 4. Verify lockfile was created
cat uv.lock | head -20

# 5. Clean up old files
rm requirements.txt requirements-dev.txt setup.py setup.cfg Pipfile Pipfile.lock

# 6. Add .venv to .gitignore
echo ".venv/" >> .gitignore

uv in Docker

The canonical pattern uses --mount=type=cache for fast rebuilds and --no-install-project for layer separation:

# Copy uv binary (pin the version)
COPY --from=ghcr.io/astral-sh/uv:0.5.27 /uv /uvx /bin/

WORKDIR /app

# Layer 1: install dependencies (changes rarely → cached aggressively)
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --locked --no-install-project --no-editable

# Layer 2: copy source and install project (changes frequently)
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-editable

Key flags:

Flag Effect
--locked Fail if uv.lock is out of date with pyproject.toml
--no-install-project Install deps but not the project itself (layer separation)
--no-editable Install in non-editable mode (copy code into .venv, not symlink)
--no-dev Skip dev dependencies (use in production images)
--compile-bytecode Pre-compile .pyc files (faster startup, larger image)

After syncing, activate the venv via PATH (not uv run) in production:

ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "my_app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Using CMD ["uv", "run", "uvicorn", ...] in production is fine but adds a small overhead and requires uv to be present in the final image.


CI/CD with uv

# GitHub Actions example
- name: Install uv
  uses: astral-sh/setup-uv@v4
  with:
    version: "0.5.27"   # pin for reproducibility

- name: Sync dependencies
  run: uv sync --locked

- name: Run tests
  run: uv run pytest --tb=short

- name: Lint
  run: uv run ruff check .

- name: Type check
  run: uv run mypy src/

.gitignore Entries for uv Projects

.venv/
__pycache__/
*.pyc
.env
dist/
*.egg-info/
.mypy_cache/
.ruff_cache/
.pytest_cache/

Do NOT ignore uv.lock — it must be committed.