I've been running uv in production Docker containers since the early versions. It's fast as hell, but Docker deployment isn't just "replace pip with uv" and call it done. You'll hit edge cases that'll make you question your life choices.
The Docker build process with uv follows a typical multi-stage pattern: dependencies first, then application code, then runtime optimization.
The big win? Our production builds went from 12+ minutes to around 3 minutes. Docker layer caching actually works now because uv's dependency resolution is deterministic. But you need to structure your Dockerfile correctly or you'll lose all those gains.
The Critical Docker Environment Variables
uv behaves differently in containers. These settings will save your ass:
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \
UV_PYTHON=python3.12 \
UV_PROJECT_ENVIRONMENT=/app \
UV_CACHE_DIR=/tmp/uv-cache
UV_LINK_MODE=copy is crucial - Docker filesystems don't support hard links properly, and uv will fail silently without this. Spent a weekend debugging that shit before figuring it out.
UV_COMPILE_BYTECODE=1 gives you faster startup times. Noticeably improved our cold start times.
UV_PYTHON_DOWNLOADS=never prevents uv from trying to download Python builds inside containers. Use the system Python you installed.
Multi-Stage Builds That Actually Work
Docker multi-stage builds follow a pattern: build dependencies in one stage, compile/install in another, then copy only runtime artifacts to the final image. This reduces image size by 70-90% and improves security by excluding build tools from production containers.
Here's the production Dockerfile pattern I use. Note the dependency separation - this is where most people fuck up:
## Stage 1: Build dependencies only
FROM python:3.12-slim AS deps
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ENV UV_LINK_MODE=copy UV_COMPILE_BYTECODE=1 UV_PYTHON_DOWNLOADS=never
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-dev --no-install-project
## Stage 2: Add application code
COPY . /src
WORKDIR /src
RUN --mount=type=cache,target=/root/.cache \
uv sync --locked --no-dev --no-editable
## Stage 3: Production runtime
FROM python:3.12-slim
COPY --from=deps /app /app
ENV PATH=/app/bin:$PATH
USER 1001
WORKDIR /app
The key insight: install dependencies first, then add your application code. This way, dependency layers only rebuild when uv.lock changes, not every time you modify your source.
Production Performance Gotchas
Cache mount location matters. I've seen builds fail because /root/.cache
doesn't exist in the container. Use an explicit mount target.
File ownership gets fucked. If your build stage runs as root but runtime uses a different user, you'll get permission denied errors. Took down staging for 2 hours once because I forgot the chown flag. Use COPY --chown=app:app
to fix ownership during copy.
Virtual environments in containers are worth it. I know it sounds redundant, but virtualenvs provide isolation and make debugging easier. Plus, uv creates them so fast it doesn't matter.
Real Error You'll Hit: "COPY failed: no such file or directory"
This happens when uv.lock or pyproject.toml isn't in your Docker build context. The fix is either:
- Move your Dockerfile to the project root
- Use
.dockerignore
correctly to include necessary files - Or copy the files explicitly in a separate layer first
Don't try to mount them from outside the build context - Docker won't let you.
Memory Limits Will Bite You
uv's parallel dependency resolution can spike memory usage. In containerized environments with memory limits, this causes OOM kills during uv sync
.
The fix: UV_CONCURRENT_DOWNLOADS=1
to serialize downloads, or increase your build container memory limits. We run builds with 2GB minimum now.
Private Package Repositories
If you're using private PyPI indexes, Docker secrets mounting works better than environment variables:
RUN --mount=type=secret,id=pip_index_url \
UV_EXTRA_INDEX_URL=$(cat /run/secrets/pip_index_url) \
uv sync --locked
Don't put authentication tokens directly in the Dockerfile - they end up in the image layers forever.
Essential Docker + uv Resources
- Official uv Docker guide - official documentation for container integration
- uv environment variables reference - complete list of configuration options for containers
- Docker BuildKit documentation - advanced build features and optimizations
- Multi-stage build best practices - Docker's official optimization guidelines
- Docker secrets management - secure handling of sensitive data in builds
- Container memory management - setting resource limits and troubleshooting OOM issues
- Python Docker best practices - comprehensive guide for Python containers
- uv-docker-example repository - official production-ready Dockerfile examples
- Dockerfile linting with hadolint - automated best practices checking for Dockerfiles
- Container security scanning - vulnerability detection for production images
- GitHub Actions Docker integration - CI/CD pipeline setup for container builds
- Production deployment troubleshooting - real-world deployment patterns and common issues