Skip to main content

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.txt with pinned versions
  • Frontend: package.json + committed package-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

QuestionViolated 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