[FastAPI Series 11] Getting Ready for Production: Configuration and Deployment

한국어 버전

Once the features are in place you need a predictable execution environment. Environment variables keep secrets out of the repo, .env files cover developer defaults, and uvicorn is the ASGI server that actually serves the FastAPI app. The fastapi dev command (often run through uv run fastapi dev) is a developer convenience around uvicorn, but production relies on calling uvicorn directly, supervising the process, and tightening configuration.

Key concepts and terms

  1. Environment variables: OS-level values for passwords, DB URLs, or feature flags that you inject without touching the source code.
  2. .env: A development-only key/value file that pydantic-settings reads so you can override defaults locally.
  3. pydantic-settings: A helper that loads environment variables, .env, and defaults in one class, which is why the post builds a Settings object with it.
  4. Process manager: Tools such as systemd or Supervisor that restart and monitor uvicorn so the API keeps running.

Practice card

  • Estimated time: 50 minutes
  • Prereqs: Part 10 auth code, .env experience
  • Goal: Read values with pydantic-settings and separate uvicorn run commands

Building a layered settings stack

pydantic-settings lets you load values in this order: in-code defaults → .env file → OS environment variables (highest priority). Keeping this hierarchy stable means you can recreate the same behavior locally and in CI, and you do not have to hand-write os.environ.get() fallbacks for every field.

OS environment variables.env fileCode defaultsSettings(BaseSettings)FastAPI + uvicorn fallbackdev overridehighest priorityinject config

Keeping this flow in mind lets you trace which value actually won and recreate the same order inside CI. BaseSettings comes from pydantic-settings, so install it (uv add pydantic-settings) and inherit from it to wire the loader.

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "sqlite:///./todo.db"
    secret_key: str
    uv_reload: bool = False
    log_level: str = "INFO"

    model_config = {
        "env_file": ".env",
        "env_prefix": "APP_",  # Reads APP_SECRET_KEY -> secret_key
    }

settings = Settings()

.env example:

APP_SECRET_KEY=please-change
APP_DATABASE_URL=postgresql+psycopg://user:pass@db/todo

Keep the .env file at the project root (the same directory where pyproject.toml lives) so relative paths resolve. Provide safe defaults for non-critical values in the code, override them in .env, and keep production secrets in real environment variables. Required secrets such as secret_key intentionally have no default so the app fails fast if they are missing. ⚠️ Never commit .env to version control—add it to .gitignore and source real secrets from your deployment environment or secrets manager. With this stack the same codebase runs safely anywhere.

uvicorn run profiles

Development favors auto reload and verbose errors. fastapi dev wraps uvicorn with --reload, so running it through uv run fastapi dev app/main.py pulls in the same virtual environment but restarts on every file change:

uv run fastapi dev app/main.py

Production needs a fixed command and external supervision so you can reason about worker counts, timeouts, and logging:

uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

For I/O-bound APIs (most REST apps) start with --workers (2 × CPU cores) + 1; for CPU-bound workloads match the exact core count. In containers call os.cpu_count() to inspect the limit, then adjust after load testing.


cores = os.cpu_count() or 1
workers = cores * 2 + 1  # I/O bound rule of thumb

If you are behind a load balancer use --proxy-headers so the original client IP survives. Keep uvicorn under systemd, Supervisor, or an orchestrator so crashes trigger automatic restarts.

Static files and media

Serve static assets through FastAPI StaticFiles only for tiny bundles (docs pages, a few CSS files under ~5 MB total). Mount them explicitly and keep paths predictable:

from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="app/static"), name="static")

For anything larger, hand the work to nginx, a CDN, or object storage so the API processes stay focused on requests. Keep CORS rules in configuration because every environment has different domains.

Logging and monitoring


logging.basicConfig(
    level=getattr(logging, settings.log_level.upper(), logging.INFO),
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

logger = logging.getLogger("todo")

Align the log format early so production logs are readable. Drive the level from settings so staging can stay verbose while production keeps noise low. If you need structured JSON later, add loguru or structlog. Attach Prometheus or OpenTelemetry exporters to produce dashboards quickly.

Containerized example

FROM ghcr.io/astral-sh/uv:python3.12-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
COPY app ./app
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Container images make CI/CD straightforward: test → build image → deploy. Shipping the same image to every server eliminates configuration drift. If you deploy to plain VMs instead, pair the earlier uvicorn command with a systemd unit and skip the container step.

Health checks and validation

Expose a fast liveness probe (/health) that simply returns {"status": "ok"} so orchestrators know the process is alive, and a readiness probe (/ready) that pings the DB or cache because those checks can take longer:

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/ready")
async def ready(session: Session = Depends(get_session)):
    session.exec("SELECT 1")
    return {"ready": True}

Keep external API calls out of readiness handlers so flaky vendors do not flap your deployment. Call Settings() (which runs validation) inside CI or a pre-deploy hook to ensure required variables exist, and fail the pipeline if validation raises.

Practice checklist

  • Follow along: refactor the Part 10 app to pull secret_key and DB URLs from the new Settings class, then read APP_SECRET_KEY from .env before running uvicorn.
  • Extend: write a Dockerfile or systemd unit so production commands live in scripts.
  • Debug: inspect ValidationError when environment variables are missing so the app reports helpful errors.
  • Done when: local and production commands diverge cleanly and configuration validation happens before deploy.

Wrap-up

Clear separation between environment variables and uvicorn run strategies keeps production behavior predictable. With this foundation the next post combines auth, data, and config into a mini service.

💬 댓글

이 글에 대한 의견을 남겨주세요