Twelve-Factor App — Applied to EduCenter
Reference: 12factor.net
I. Codebase — One codebase, many deploys
Single git repository. Same code deploys to local dev and production. Never maintain separate codebases per environment.
II. Dependencies — Explicitly declare and isolate
- Backend:
requirements.txtwith pinned versions - Frontend:
package.json+ committedpackage-lock.json - Infrastructure:
docker-compose.yml/docker-compose.prod.yml - Python dependencies isolated inside Docker container (no host-level installs)
III. Config — Store config in the environment
All deployment-specific config in env vars. Pydantic Settings reads from environment at startup. No config.production.py.
# Same code, different environment
DATABASE_URL=postgresql+asyncpg://localhost/platform # dev
DATABASE_URL=postgresql+asyncpg://prod-db/platform # prod
IV. Backing Services — Attached resources via URL
PostgreSQL, Redis, and LXD are attached resources. Swap providers by changing one env var. No code changes needed.
V. Build, Release, Run — Strictly separate
- Build:
docker build— compile, bundle, lint → Docker image - Release: Image + env config → deployed container
- Run:
docker-compose up— execute the release. No compilation at runtime.
VI. Processes — Stateless
Backend is stateless. All persistent state in PostgreSQL and Redis. Container info is stored in the containers table, not in memory.
Exception: LXD containers are stateful by nature (they run on a specific server), but container state is tracked in the database.
VII. Port Binding — Self-contained
Backend binds to port 8000. Frontend static files served by Nginx on port 80/443. No external runtime dependencies beyond what's in the container.
VIII. Concurrency — Scale via process model
More backend instances = more capacity (stateless design). Background tasks (container cleanup, lesson repo refresh) run via FastAPI startup events or scheduled jobs.
IX. Disposability — Fast startup, graceful shutdown
Backend starts in < 5 seconds. Handles SIGTERM gracefully. Docker restart policy ensures auto-recovery.
X. Dev/Prod Parity — Same services everywhere
docker-compose.yml runs the same PostgreSQL and Redis as production. No SQLite in dev, no in-memory substitutes.
XI. Logs — Event streams to stdout
All logs to stdout via Python logging. Docker captures and routes them. No log files written by the application.
# View logs
docker-compose logs -f backend
docker-compose -f docker-compose.prod.yml logs -f backend
XII. Admin Processes — One-off commands
# Migrations — run separately, not on app startup
docker-compose exec backend alembic upgrade head
# Database seed
docker-compose exec backend python -m app.db.init_db
# Never embedded in main.py startup
Quick Compliance Check
| Question | Violated if NO |
|---|---|
| Can I change the DB URL without changing code? | III. Config |
| Can I swap PostgreSQL providers by changing one env var? | IV. Backing Services |
| Can I run two backend instances simultaneously? | VI. Processes |
| Does the app write logs only to stdout? | XI. Logs |
| Do migrations run as a separate command, not on startup? | XII. Admin Processes |
| Does Docker Compose match production topology? | X. Dev/Prod Parity |