Packaging & Deployment — The Complete Notebook
Comprehensive guide on Packaging & Deployment — The Complete Notebook.
Packaging & Deployment
1. Overview#
Writing working code is only part of the job — it also needs to be packaged in a way others can install, and deployed somewhere it can actually run reliably. This note covers modern Python packaging (pyproject.toml), containerizing an app with Docker, and the basics of getting a Python service into production.
2. Modern Packaging with pyproject.toml#
pyproject.toml is the modern, standardized way to define a Python project's metadata and dependencies — replacing the older setup.py/requirements.txt-only approach.
2.1 A Basic pyproject.toml#
toml[project]
name = "my-api"
version = "1.0.0"
description = "A FastAPI service for order management"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.111.0",
"uvicorn[standard]>=0.30.0",
"sqlalchemy>=2.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"mypy>=1.10",
"ruff>=0.5",
]
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"
2.2 Installing From It#
bashpip install . # install the project itself
pip install ".[dev]" # install with the optional "dev" dependency group
pip install -e . # editable install — for actively developing the package
2.3 Project Layout#
codemy-api/ ├── pyproject.toml ├── README.md ├── .gitignore ├── src/ │ └── my_api/ │ ├── __init__.py │ └── main.py └── tests/ └── test_main.py
The
src/layout (code insidesrc/my_api/rather than directly at the project root) is now widely recommended — it prevents accidentally importing your package from the wrong location during testing.
3. Environment & Dependency Management Tools#
| Tool | What It Adds Over Plain pip |
|---|---|
venv + pip | Built-in, minimal, works everywhere |
poetry | Dependency resolution, lock files, publishing, all via pyproject.toml |
uv | Extremely fast installs/resolution, drop-in pip/venv replacement |
pipenv | Combines pip + virtualenv management with a Pipfile.lock |
bash# Example with poetry
poetry init # interactive pyproject.toml setup
poetry add fastapi # adds and locks a dependency
poetry install # installs everything from the lock file
poetry run uvicorn my_api.main:app
4. Environment Variables & Configuration#
4.1 .env Files for Local Development#
Mathematical Formulation# .env DATABASE_URL=postgresql://user:pass@localhost/mydb API_KEY=dev-secret-key DEBUG=true
🐍 PythonInteractive WebAssemblyimport os
from dotenv import load_dotenv
load_dotenv()
DATABASE_URL = os.environ["DATABASE_URL"]
Never commit
.envto version control — add it to.gitignore. In production, environment variables are typically injected by the hosting platform (Docker, Kubernetes secrets, cloud provider config) rather than read from a file.
4.2 Typed Settings with Pydantic#
🐍 PythonInteractive WebAssemblyfrom pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings() # automatically reads and validates from environment/.env
5. Containerizing with Docker#
5.1 A Basic Dockerfile for a FastAPI App#
dockerfileFROM python:3.12-slim WORKDIR /app # Copy dependency files first — leverages Docker layer caching COPY pyproject.toml . RUN pip install --no-cache-dir . # Copy the rest of the application code COPY src/ ./src/ EXPOSE 8000 CMD ["uvicorn", "src.my_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
5.2 Building & Running#
bashdocker build -t my-api:latest .
docker run -p 8000:8000 --env-file .env my-api:latest
5.3 docker-compose for Multi-Service Apps#
yaml# docker-compose.yml
services:
api:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
bashdocker-compose up --build
5.4 Multi-Stage Builds (Smaller Production Images)#
dockerfile# Stage 1: build dependencies FROM python:3.12-slim AS builder WORKDIR /app COPY pyproject.toml . RUN pip install --no-cache-dir --target=/app/deps . # Stage 2: minimal final image FROM python:3.12-slim WORKDIR /app COPY --from=builder /app/deps /usr/local/lib/python3.12/site-packages COPY src/ ./src/ CMD ["uvicorn", "src.my_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
6. Running in Production#
6.1 uvicorn with Multiple Workers#
bashuvicorn my_api.main:app --host 0.0.0.0 --port 8000 --workers 4
6.2 gunicorn as a Process Manager (Common Pattern with FastAPI)#
bashpip install gunicorn
gunicorn my_api.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Gunicorn manages multiple uvicorn worker processes — restarting crashed workers, load-balancing requests across them.
6.3 Health Checks#
🐍 PythonInteractive WebAssembly@app.get("/health")
def health_check():
return {"status": "ok"}
Most orchestration platforms (Kubernetes, load balancers) poll an endpoint like this to know whether an instance is ready to receive traffic.
7. CI/CD Basics (GitHub Actions Example)#
yaml# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ".[dev]"
- run: pytest
- run: mypy src/
This automatically runs the test suite and type checks on every push/PR — catching issues before they merge.
8. Common Pitfalls#
INCORRECT: Copying the Entire Project Before Installing Dependencies in Docker#
dockerfileCOPY . . RUN pip install .
Every code change invalidates Docker's cache and forces a full dependency reinstall.
CORRECT: Fix — Copy Dependency Files First#
dockerfileCOPY pyproject.toml . RUN pip install --no-cache-dir . COPY . .
INCORRECT: Running as Root Inside a Container#
dockerfile# No USER instruction — container runs as root by default, a security risk
CORRECT: Fix#
dockerfileRUN useradd --create-home appuser USER appuser
INCORRECT: Baking Secrets Into a Docker Image#
dockerfileENV API_KEY=sk-abc123 # visible to anyone who inspects the image layers
CORRECT: Fix — Inject at Runtime#
bashdocker run --env-file .env my-api:latest
9. Summary & Best Practices Checklist#
- Use
pyproject.tomlfor new projects instead of a barerequirements.txt. - Use the
src/layout to avoid import-path issues during testing. - Never commit
.envfiles or secrets — inject them at runtime via environment variables. - Order Dockerfile steps so dependency installation is cached separately from code changes.
- Run containers as a non-root user.
- Use multi-stage Docker builds to keep production images small.
- Add a
/healthendpoint for orchestration platforms to monitor. - Automate tests and type checks in CI so issues are caught before deployment.
Packaging & Deployment Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.