uv — Using uv in Docker
Dockerfile untuk aplikasi Python modern: multi-stage, cache layer yang benar, dan image production yang ramping.
Intisari
- Salin
pyproject.tomldanuv.locksebelum kode aplikasi — supaya layer dependency bisa di-cache. uv sync --frozen --no-devdi production: gagal kalau lockfile tidak sinkron, dan tidak memasangpytest.- Multi-stage build: builder memasang dependency, image akhir hanya menyalin hasilnya.
- Jalankan sebagai non-root, dan letakkan
.venv/bindiPATH. .dockerignoreyang baik memangkas ukuran konteks build secara drastis.
Dockerfile lengkap
# ── Stage 1: builder ─────────────────────────────────
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never
WORKDIR /app
# 1. Dependency dulu — layer ini di-cache selama lockfile tidak berubah
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# 2. Baru kode aplikasi
COPY src/ ./src/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# ── Stage 2: runtime ─────────────────────────────────
FROM python:3.13-slim
RUN useradd --create-home --uid 1000 app
WORKDIR /app
COPY --from=builder --chown=app:app /app/.venv /app/.venv
COPY --from=builder --chown=app:app /app/src /app/src
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]
Kenapa urutannya begitu
Docker meng-cache layer, dan cache batal begitu ada file yang berubah.
Kode aplikasimu berubah puluhan kali sehari; dependency-mu mungkin sebulan sekali.
Dengan menyalin pyproject.toml dan uv.lock lebih dulu,
layer pemasangan dependency tetap ter-cache — dan build berikutnya hanya butuh beberapa detik
alih-alih beberapa menit.
# ❌ salah — tiap perubahan kode memicu install ulang semua dependency
COPY . .
RUN uv sync --frozen
# ✅ benar
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev
COPY src/ ./src/
RUN uv sync --frozen --no-dev
Flag yang penting
| Flag | Fungsinya |
|---|---|
--frozen | Gagal kalau uv.lock tidak cocok dengan pyproject.toml — build jadi reprodusibel |
--no-dev | Lewati pytest, mypy, dll — image lebih kecil |
--no-install-project | Hanya dependency, belum kode proyeknya (untuk pemisahan layer) |
UV_COMPILE_BYTECODE=1 | Pre-compile .pyc — startup lebih cepat |
UV_LINK_MODE=copy | Salin, jangan hardlink — hardlink tidak bekerja lintas layer |
UV_PYTHON_DOWNLOADS=never | Pakai Python dari image dasar, jangan unduh lagi |
Cache mount
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
Cache download uv bertahan antar build tanpa masuk ke dalam image. Butuh BuildKit (default di Docker modern). Ini bisa memangkas waktu build secara signifikan.
.dockerignore
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
.git/
.github/
tests/
docs/
*.md
.env
.env.*
data/
*.db
.DS_Store
Menyertakan .venv/ ke konteks build adalah kesalahan yang sangat umum.
Foldernya besar, spesifik OS, dan akan ditimpa oleh uv sync di dalam container.
Ia hanya memperlambat build. Sama pentingnya: .env harus di-ignore supaya kredensial
tidak ikut ke image.
Jangan jalankan sebagai root
RUN useradd --create-home --uid 1000 app
COPY --from=builder --chown=app:app /app/.venv /app/.venv
USER app
Kalau ada kerentanan di aplikasimu, penyerang mendapat hak akses user biasa — bukan root di dalam container.
Environment variable
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
| Variabel | Kenapa |
|---|---|
PATH ke .venv/bin | Supaya uvicorn dan python memakai venv, tanpa uv run |
PYTHONUNBUFFERED=1 | Wajib — tanpa ini log tertahan di buffer dan tidak muncul |
PYTHONDONTWRITEBYTECODE=1 | Jangan tulis .pyc saat runtime (sudah di-compile saat build) |
Rahasia
# ❌ JANGAN — key ikut tersimpan permanen di layer image
ENV ANTHROPIC_API_KEY=sk-ant-xxx
# ✅ saat runtime
services:
api:
image: myapp:latest
environment:
- MAX_TOKENS=8192
secrets:
- anthropic_api_key
secrets:
anthropic_api_key:
file: ./secrets/anthropic_api_key
class Settings(BaseSettings):
model_config = SettingsConfigDict(secrets_dir="/run/secrets")
anthropic_api_key: SecretStr
Layer image bersifat permanen. Menghapus rahasia di layer berikutnya tidak menghapusnya dari layer sebelumnya — siapa pun yang punya image itu bisa mengambilnya kembali. Rahasia hanya boleh masuk saat runtime.
Docker Compose untuk development
services:
api:
build: .
ports:
- "8000:8000"
environment:
- AWS_REGION=us-east-1
env_file:
- .env
volumes:
- ./src:/app/src # hot reload saat development
command: uvicorn myapp.main:app --host 0.0.0.0 --reload
depends_on:
chroma:
condition: service_healthy
chroma:
image: chromadb/chroma:latest
volumes:
- chroma-data:/chroma/chroma
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v2/heartbeat"]
interval: 10s
volumes:
chroma-data:
Memeriksa hasilnya
docker build -t myapp .
docker images myapp # cek ukuran — target di bawah 300 MB
docker run --rm -p 8000:8000 --env-file .env myapp
docker history myapp # lihat kontribusi tiap layer
Kalau image-mu kegedean
- Pastikan memakai multi-stage — jangan sertakan toolchain build di image akhir
- Pastikan
--no-devdipakai - Periksa
.dockerignore - Pakai
python:3.13-slim, bukan tag penuh - Cek apakah ada dependency berat yang sebenarnya tidak dipakai (
torch,pandasdi production)
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.