Feature Image 2

# A 12 Factor Crash Course in Python: Build Clean, Scalable FastAPI Apps the Right Way

Table of Contents

a 12-factor app crash course
a 12-factor app crash course

Intro: Building Apps That Don’t Suck in Production

Let’s be honest—plenty of apps “work on my machine” but self-destruct the moment they meet the real world. Configs hardcoded, logs missing, environments confused, and deployments that feel like an escape room puzzle.

If you want your service to thrive in production (and not become an ops horror story), you need a design philosophy that enforces clean separation, modularity, and resilience. That’s where the 12 Factor App methodology comes in.

In this post, we’re going to break down each of the 12 Factor using a Python/FastAPI related stack—and walk through how to get them right.

🧱 The Twelve Factor — Python Style

Let’s take each principle, one by one. Think of it as a devops dojo, with Python as your katana.


Codebase: One codebase tracked in revision control, many deploys

12 Factor App: Single source of truth, version-controlled, no Franken-repos.

📌 In Python:

  • One Git repo per service.

  • Don’t share code across projects via copy-paste. Use internal packages or shared libraries (published to private PyPI or via Git submodules).

Best Practice:

/fastapi-12factor-app
├── app/
│ ├── api/
│ ├── core/
│ ├── models/
│ └── main.py
├── tests/
├── Dockerfile
├── pyproject.toml
├── README.md
└── .env

Dependencies: Explicitly declare and isolate dependencies

12 Factor App: No implicit magic. Use virtualenvs and lock your deps.

📌 In Python: Use pyproject.toml and a tool like Poetry or pip-tools.

Example pyproject.toml:

[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.110.0"
uvicorn = "^0.29.0"
sqlalchemy = "^2.0"
pydantic = "^2.6"
python-dotenv = "^1.0"

🔒 Lock it down:

poetry lock

And run your app in a containerized environment, so your coworker’s Python 3.6 setup doesn’t eat your soul.

Config: Store config in the environment

Configs aren’t code. Environment variables FTW.

📌 In Python with Pydantic v2:

from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()

.env for local:

DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/app
DEBUG=true

🚀 Let Kubernetes inject real env vars in prod. No secrets in code, please.


Backing Services: Treat backing services as attached resources

12 Factor App: Databases, queues, and blobs should be replaceable.

📌 In FastAPI:

Define your database URL in settings.database_url, not hardcoded. SQLAlchemy supports this beautifully.

from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(settings.database_url, echo=settings.debug)

🧪 In test, you can override DATABASE_URL with a SQLite memory DB. That’s the power of this separation.


Build, Release, Run: Strictly separate build and run stages

12 Factor App: Immutable images. Don’t change code/configs post-build.

📦 Dockerfile example:

FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install poetry && poetry install --no-dev
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

👊 Don’t inject secrets during build—use env at runtime.


Processes: Execute the app as one or more stateless processes

12 Factor App: Stateless, share-nothing services.

📌 In FastAPI:

  • Keep state (like DB sessions) outside the app object.

  • Use dependency injection for scoped connections.

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
async def get_session() -> AsyncSession:
async with async_session() as session:
yield session

This plays nice with Kubernetes autoscaling and kills zombie state.


Port Binding: Export services via port binding

12 Factor App: Your app should be self-contained and listen on a port.

FastAPI does this naturally:

uvicorn app.main:app --host 0.0.0.0 --port 8000

K8s service can bind this to external ports as needed. No Apache/Nginx glue required.


Concurrency: Scale out via the process model

12 Factor App: Scale horizontally, not by making megathreads.

📌 Use Uvicorn workers via gunicorn if needed, or just scale pods in K8s:

gunicorn -k uvicorn.workers.UvicornWorker app.main:app -w 4

Or define a HorizontalPodAutoscaler in K8s—clean separation.


Disposability: Fast startup and graceful shutdown

12 Factor App: Apps should start/stop fast and cleanly.

In FastAPI, use startup/shutdown events:

from fastapi import FastAPI
app = FastAPI()
@app.on_event("startup")
async def on_startup():
print("Ready to go!")
@app.on_event("shutdown")
async def on_shutdown():
print("Shutting down gracefully...")

Kubernetes will send SIGTERM—be ready for it.


Dev/Prod Parity: Keep development, staging, and production as similar as possible

📌 Use .env for local, ConfigMaps/Secrets for prod, but same app code.

Also—use Docker for dev, same as prod. Don’t “just run it on the host.”

Use docker-compose in dev (or Tilt/Skaffold) to mirror the prod infra.


Logs: Treat logs as event streams

12 Factor App: Don’t write to files. Stream to stdout/stderr.

FastAPI + logging setup:

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.get("/health")
async def health():
logger.info("Health check called")
return {"status": "ok"}

🎯 Let Kubernetes + Fluentd/ELK/Grafana Loki deal with aggregation.


Admin Processes: Run admin/one-off tasks as one-off processes

12 Factor App: Create a separate scripts/ dir with admin tasks (DB migrations, data cleaning, etc.)

/scripts/
└── migrate.py

Run it as:

python scripts/migrate.py

Or use K8s Jobs for one-offs in production.

Cheatsheet

FactorApplies ToDescription
CodebaseAll appsOne codebase per app, tracked in version control, with many deploys.
DependenciesLanguage/runtimeExplicitly declare and isolate dependencies via a manifest (e.g., pyproject.toml).
ConfigEnvironment managementStore config in environment variables; never in code.
Backing ServicesDatabases, queues, cachesTreat services like resources; attach/detach them via config, not code changes.
Build, Release, RunCI/CD pipelinesSeparate build, release, and run stages. Never change code/config after release.
ProcessesApplication executionExecute apps as stateless processes; share nothing, scale horizontally.
Port BindingWeb servicesExport services via port binding; don’t depend on external web servers.
ConcurrencyScalabilityScale out via process model; use multiple instances or pods, not threads.
DisposabilityLifecycle managementFast startup and graceful shutdown improve robustness and scalability.
Dev/Prod ParityDev environmentsKeep development, staging, and production as similar as possible.
LogsObservabilityTreat logs as event streams; write to stdout/stderr and let the platform handle aggregation.
Admin ProcessesOne-off tasksRun one-off admin tasks (e.g., migrations) as isolated processes, not part of the main app.

🔚 Wrapping It All Up

The 12 Factor App methodology isn’t just a checklist—it’s a survivability manual for cloud-native apps. And FastAPI, paired with Pydantic v2 and SQLAlchemy, makes following these principles refreshingly clean.

A few takeaways:

  • Treat config like royalty—never hardcode it.

  • Keep your app stateless and dumb—let Kubernetes do the smart scaling.

  • Stream your logs, don’t hoard them.

  • Build once, deploy often, break never (hopefully).

:::successIf you want to check more about engineering notes and system design, feel free to visit the tags at here:::

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts