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:
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.
Build, Release, Run: Strictly separate build and run stages
12 Factor App: Immutable images. Don’t change code/configs post-build.
📦 Dockerfile example:
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:
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:
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
Factor | Applies To | Description |
---|---|---|
Codebase |
All apps | One codebase per app, tracked in version control, with many deploys. |
Dependencies |
Language/runtime | Explicitly declare and isolate dependencies via a manifest (e.g., pyproject.toml ). |
Config |
Environment management | Store config in environment variables; never in code. |
Backing Services |
Databases, queues, caches | Treat services like resources; attach/detach them via config, not code changes. |
Build, Release, Run |
CI/CD pipelines | Separate build, release, and run stages. Never change code/config after release. |
Processes |
Application execution | Execute apps as stateless processes; share nothing, scale horizontally. |
Port Binding |
Web services | Export services via port binding; don’t depend on external web servers. |
Concurrency |
Scalability | Scale out via process model; use multiple instances or pods, not threads. |
Disposability |
Lifecycle management | Fast startup and graceful shutdown improve robustness and scalability. |
Dev/Prod Parity |
Dev environments | Keep development, staging, and production as similar as possible. |
Logs |
Observability | Treat logs as event streams; write to stdout/stderr and let the platform handle aggregation. |
Admin Processes |
One-off tasks | Run 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).