
12 Factor App
- 252 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
12-factor-app: A skill for development. This provides functionality for development workflows.
Key points
- 12-factor-app
12 Factor App by the numbers
- 252 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,504 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill 12-factor-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 252 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use 12-factor-app for development tasks?
Use 12-factor-app for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with 12-factor-app.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use 12-factor-app for development tasks, or when 12-factor-app: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to 12-factor-app: 12-factor-app.
Files
Community Cloud-Native Applications Best Practices
Comprehensive methodology for building modern software-as-a-service applications that are portable, scalable, and maintainable. Contains 51 rules across 12 categories, covering the entire application lifecycle from codebase management to production operations.
When to Apply
Reference these guidelines when:
- Designing new backend services or APIs
- Containerizing applications for Kubernetes or Docker
- Setting up CI/CD pipelines
- Managing configuration across environments
- Implementing logging and monitoring
- Planning application scaling strategy
- Debugging deployment or environment issues
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Codebase & Version Control | CRITICAL | code- |
| 2 | Dependencies | CRITICAL | dep- |
| 3 | Configuration | CRITICAL | config- |
| 4 | Backing Services | HIGH | svc- |
| 5 | Build, Release, Run | HIGH | build- |
| 6 | Processes & State | HIGH | proc- |
| 7 | Concurrency & Scaling | HIGH | scale- |
| 8 | Disposability | HIGH | disp- |
| 9 | Port Binding | MEDIUM | port- |
| 10 | Dev/Prod Parity | MEDIUM | parity- |
| 11 | Logging | MEDIUM | log- |
| 12 | Admin Processes | MEDIUM | admin- |
Quick Reference
1. Codebase & Version Control (CRITICAL)
- `code-single-codebase` - Maintain one codebase per application in version control
- `code-one-app-one-repo` - Enforce one-to-one correlation between codebase and application
- `code-deploys-not-branches` - Use deploys not branches to represent environments
- `code-shared-as-libraries` - Factor shared code into libraries managed by dependency manager
2. Dependencies (CRITICAL)
- `dep-explicit-declaration` - Declare all dependencies explicitly in a manifest file
- `dep-isolate-execution` - Isolate dependencies to prevent system package leakage
- `dep-no-system-tools` - Never rely on implicit system tools being available
- `dep-deterministic-builds` - Use lockfiles for deterministic dependency resolution
3. Configuration (CRITICAL)
- `config-separate-from-code` - Strictly separate configuration from code
- `config-use-env-vars` - Store configuration in environment variables
- `config-no-env-groups` - Treat environment variables as granular controls not grouped environments
- `config-validate-on-startup` - Validate required configuration at application startup
- `config-never-commit-secrets` - Never commit secrets or credentials to version control
4. Backing Services (HIGH)
- `svc-as-attached-resources` - Treat backing services as attached resources
- `svc-connection-strings` - Reference all backing services via connection URLs in config
- `svc-no-local-vs-remote` - Make no distinction between local and third-party services
- `svc-detach-attach-without-code` - Design services to be detachable and attachable without code changes
5. Build, Release, Run (HIGH)
- `build-separate-stages` - Strictly separate build, release, and run stages
- `build-immutable-releases` - Create immutable releases with unique identifiers
- `build-no-runtime-changes` - Never modify code at runtime - changes require new release
- `build-complexity-in-build` - Push complexity into build stage keep run stage minimal
- `build-artifact-per-commit` - Generate one build artifact per commit deploy same artifact everywhere
6. Processes & State (HIGH)
- `proc-stateless-processes` - Execute the application as stateless processes
- `proc-no-sticky-sessions` - Never use sticky sessions - store session data in backing services
- `proc-no-local-filesystem` - Never assume local filesystem persists between requests
- `proc-compile-at-build` - Perform asset compilation and bundling at build time not runtime
- `proc-share-nothing` - Design processes to share nothing with each other
7. Concurrency & Scaling (HIGH)
- `scale-process-model` - Scale out via the process model with multiple process types
- `scale-process-types` - Assign workloads to appropriate process types
- `scale-no-daemonize` - Never daemonize or write PID files let process manager handle it
- `scale-horizontal-not-vertical` - Design for horizontal scaling over vertical scaling
- `scale-process-formation` - Define process formation as declarative configuration
8. Disposability (HIGH)
- `disp-disposable-processes` - Design processes to be disposable started or stopped at any moment
- `disp-fast-startup` - Minimize startup time to enable rapid scaling and recovery
- `disp-graceful-shutdown` - Implement graceful shutdown on SIGTERM
- `disp-crash-only` - Design for crash-only software that recovers from sudden death
- `disp-idempotent-operations` - Make operations idempotent to safely retry after failures
9. Port Binding (MEDIUM)
- `port-self-contained` - Make the application completely self-contained with embedded server
- `port-export-via-binding` - Export services via port binding using PORT environment variable
- `port-any-protocol` - Use port binding to export any protocol not just HTTP
10. Dev/Prod Parity (MEDIUM)
- `parity-minimize-gaps` - Minimize gaps between development and production environments
- `parity-same-backing-services` - Use the same type and version of backing services in all environments
- `parity-deploy-frequently` - Deploy frequently to minimize the time gap
- `parity-developers-deploy` - Involve developers in deployment to minimize personnel gap
11. Logging (MEDIUM)
- `log-event-streams` - Treat logs as event streams not files
- `log-no-routing` - Never route or store logs from within the application
- `log-structured-format` - Use structured logging for machine-readable event streams
- `log-unbuffered-stdout` - Write logs unbuffered to stdout for real-time streaming
12. Admin Processes (MEDIUM)
- `admin-one-off-processes` - Run admin tasks as one-off processes not special scripts
- `admin-same-environment` - Run admin processes against a release with same codebase and config
- `admin-repl-access` - Provide REPL access for debugging and data inspection
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Cloud-Native Applications
Version 0.1.0 Twelve-Factor Community January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
The Twelve-Factor App methodology for building modern, scalable software-as-a-service applications. Contains 51 rules across 12 categories covering codebase management, dependency isolation, configuration, backing services, build/release/run separation, stateless processes, port binding, concurrency, disposability, dev/prod parity, logging, and admin processes. Each rule provides actionable guidance for AI agents to generate cloud-native, deployment-ready code.
---
Table of Contents
1. Codebase & Version Control — CRITICAL
- 1.1 Enforce One-to-One Correlation Between Codebase and Application — CRITICAL (prevents coupling, enables independent deployment)
- 1.2 Factor Shared Code Into Libraries Managed by Dependency Manager — HIGH (enables code reuse without coupling, allows independent versioning)
- 1.3 Maintain One Codebase Per Application in Version Control — CRITICAL (enables consistent deployments, prevents configuration drift)
- 1.4 Use Deploys Not Branches to Represent Environments — HIGH (prevents environment-specific code paths, simplifies merging)
2. Dependencies — CRITICAL
- 2.1 Declare All Dependencies Explicitly in a Manifest File — CRITICAL (enables reproducible builds, prevents "works on my machine" issues)
- 2.2 Isolate Dependencies to Prevent System Package Leakage — CRITICAL (prevents version conflicts, ensures consistent behavior across environments)
- 2.3 Never Rely on Implicit System Tools Being Available — HIGH (ensures portability, prevents deployment failures)
- 2.4 Use Lockfiles for Deterministic Dependency Resolution — HIGH (guarantees identical builds, prevents "it worked yesterday" bugs)
3. Configuration — CRITICAL
- 3.1 Never Commit Secrets or Credentials to Version Control — CRITICAL (prevents security breaches, credentials in git history are nearly impossible to fully remove)
- 3.2 Store Configuration in Environment Variables — CRITICAL (language-agnostic, impossible to accidentally commit, easy to change per deploy)
- 3.3 Strictly Separate Configuration from Code — CRITICAL (enables deployment to any environment without code changes)
- 3.4 Treat Environment Variables as Granular Controls Not Grouped Environments — HIGH (scales to unlimited deploys, prevents combinatorial explosion)
- 3.5 Validate Required Configuration at Application Startup — HIGH (fast failure prevents silent misconfiguration, improves debugging)
4. Backing Services — HIGH
- 4.1 Design Services to Be Detachable and Attachable Without Code Changes — HIGH (enables zero-downtime migrations, disaster recovery, and scaling)
- 4.2 Make No Distinction Between Local and Third-Party Services — HIGH (ensures code portability, enables seamless service migration)
- 4.3 Reference All Backing Services via Connection URLs in Config — HIGH (standardized format, easy to swap services, works across all platforms)
- 4.4 Treat Backing Services as Attached Resources — HIGH (enables swapping services without code changes, improves resilience)
5. Build, Release, Run — HIGH
- 5.1 Create Immutable Releases with Unique Identifiers — HIGH (enables reliable rollbacks, audit trails, and deployment tracking)
- 5.2 Generate One Build Artifact Per Commit Deploy Same Artifact Everywhere — MEDIUM-HIGH (guarantees staging tests what production runs, eliminates build inconsistency)
- 5.3 Never Modify Code at Runtime - Changes Require New Release — HIGH (prevents configuration drift, ensures reproducibility)
- 5.4 Push Complexity Into Build Stage Keep Run Stage Minimal — HIGH (reduces runtime failures, faster recovery from crashes)
- 5.5 Strictly Separate Build, Release, and Run Stages — HIGH (enables rollbacks, prevents runtime modifications, improves reliability)
6. Processes & State — HIGH
- 6.1 Design Processes to Share Nothing with Each Other — HIGH (enables independent scaling, prevents cascade failures)
- 6.2 Execute the Application as Stateless Processes — HIGH (enables horizontal scaling, ensures resilience to process crashes)
- 6.3 Never Assume Local Filesystem Persists Between Requests — HIGH (prevents data loss on restart, enables containerized deployment)
- 6.4 Never Use Sticky Sessions - Store Session Data in Backing Services — HIGH (enables load balancer flexibility, prevents single-point-of-failure)
- 6.5 Perform Asset Compilation and Bundling at Build Time Not Runtime — MEDIUM-HIGH (ensures fast startup, prevents runtime compilation failures)
7. Concurrency & Scaling — HIGH
- 7.1 Assign Workloads to Appropriate Process Types — HIGH (optimizes resource usage, enables targeted scaling)
- 7.2 Define Process Formation as Declarative Configuration — MEDIUM-HIGH (enables reproducible deployments, infrastructure as code)
- 7.3 Design for Horizontal Scaling Over Vertical Scaling — HIGH (enables cost-effective scaling, eliminates single points of failure)
- 7.4 Never Daemonize or Write PID Files Let Process Manager Handle It — HIGH (enables process manager control, proper signal handling, crash recovery)
- 7.5 Scale Out via the Process Model with Multiple Process Types — HIGH (enables horizontal scaling, matches workload diversity to process types)
8. Disposability — HIGH
- 8.1 Design for Crash-Only Software That Recovers from Sudden Death — HIGH (ensures resilience to hardware failures, prevents data loss)
- 8.2 Design Processes to Be Disposable Started or Stopped at Any Moment — HIGH (enables rapid deployment, elastic scaling, and fault tolerance)
- 8.3 Implement Graceful Shutdown on SIGTERM — HIGH (prevents request failures during deploys, ensures data integrity)
- 8.4 Make Operations Idempotent to Safely Retry After Failures — HIGH (enables automatic retry, prevents duplicate processing)
- 8.5 Minimize Startup Time to Enable Rapid Scaling and Recovery — HIGH (enables autoscaling responsiveness, faster deployments, quicker crash recovery)
9. Port Binding — MEDIUM
- 9.1 Export Services via Port Binding Using PORT Environment Variable — MEDIUM (enables platform-managed port assignment, essential for container orchestration)
- 9.2 Make the Application Completely Self-Contained with Embedded Server — MEDIUM (simplifies deployment, removes webserver injection dependency)
- 9.3 Use Port Binding to Export Any Protocol Not Just HTTP — MEDIUM (enables microservices to be backing services for each other)
10. Dev/Prod Parity — MEDIUM
- 10.1 Deploy Frequently to Minimize the Time Gap — MEDIUM (reduces risk per deploy, accelerates feedback loops)
- 10.2 Involve Developers in Deployment to Minimize Personnel Gap — MEDIUM (faster issue resolution, better understanding of production behavior)
- 10.3 Minimize Gaps Between Development and Production Environments — MEDIUM (prevents "works on my machine" bugs, enables continuous deployment)
- 10.4 Use the Same Type and Version of Backing Services in All Environments — MEDIUM (eliminates environment-specific bugs, ensures production behavior in development)
11. Logging — MEDIUM
- 11.1 Never Route or Store Logs from Within the Application — MEDIUM (simplifies app code, enables flexible log infrastructure)
- 11.2 Treat Logs as Event Streams Not Files — MEDIUM (enables log aggregation, real-time analysis, cloud-native deployment)
- 11.3 Use Structured Logging for Machine-Readable Event Streams — MEDIUM (enables field-based querying, aggregation, and automated alerting)
- 11.4 Write Logs Unbuffered to Stdout for Real-Time Streaming — MEDIUM (enables real-time log viewing, prevents log loss on crash)
12. Admin Processes — MEDIUM
- 12.1 Provide REPL Access for Debugging and Data Inspection — MEDIUM (enables interactive debugging, safe data investigation)
- 12.2 Run Admin Processes Against a Release with Same Codebase and Config — MEDIUM (prevents synchronization issues, ensures correct behavior)
- 12.3 Run Admin Tasks as One-Off Processes Not Special Scripts — MEDIUM (ensures consistency, enables auditability, prevents configuration drift)
---
References
1. https://12factor.net/ 2. https://github.com/twelve-factor/twelve-factor 3. https://www.heroku.com/ 4. https://kubernetes.io/docs/concepts/ 5. https://www.docker.com/
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
{Rule Title}
{1-3 sentences explaining WHY this matters for cloud-native applications. Focus on scalability, portability, or operational implications.}
Incorrect ({what's wrong}):
```{language} {Bad code example - production-realistic, not strawman} {# Comments explaining the cost or problem}
**Correct ({what's right}):**
{Good code example - minimal diff from incorrect} {# Comments explaining the benefit}
{Optional sections as needed:}
**Alternative ({context}):**
{Alternative approach when applicable}
**When NOT to use this pattern:**
- {Exception 1}
- {Exception 2}
**Benefits:**
- {Benefit 1}
- {Benefit 2}
Reference: [The Twelve-Factor App](https://12factor.net/)
{
"name": "12-factor-app",
"version": "1.1.5",
"organization": "Twelve-Factor Community",
"technology": "Cloud-Native Applications",
"date": "January 2026",
"abstract": "The Twelve-Factor App methodology for building modern, scalable software-as-a-service applications. Contains 51 rules across 12 categories covering codebase management, dependency isolation, configuration, backing services, build/release/run separation, stateless processes, port binding, concurrency, disposability, dev/prod parity, logging, and admin processes. Each rule provides actionable guidance for AI agents to generate cloud-native, deployment-ready code.",
"references": [
"https://12factor.net/",
"https://github.com/twelve-factor/twelve-factor",
"https://www.heroku.com/",
"https://kubernetes.io/docs/concepts/",
"https://www.docker.com/"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Codebase & Version Control (code)
Impact: CRITICAL Description: One codebase tracked in revision control, many deploys. The foundation for consistent deployments and team collaboration.
2. Dependencies (dep)
Impact: CRITICAL Description: Explicitly declare and isolate all dependencies. Never rely on implicit system-wide packages or tools.
3. Configuration (config)
Impact: CRITICAL Description: Store configuration in environment variables. Strict separation of config from code enables deployment flexibility.
4. Backing Services (svc)
Impact: HIGH Description: Treat backing services as attached resources. Access databases, caches, and queues via URLs stored in configuration.
5. Build, Release, Run (build)
Impact: HIGH Description: Strictly separate build, release, and run stages. Each release is immutable and uniquely identified.
6. Processes & State (proc)
Impact: HIGH Description: Execute the app as stateless processes. Store persistent data in backing services, never in local filesystem or memory.
7. Concurrency & Scaling (scale)
Impact: HIGH Description: Scale out via the process model. Assign workloads to process types and let the execution environment manage processes.
8. Disposability (disp)
Impact: HIGH Description: Maximize robustness with fast startup and graceful shutdown. Processes are disposable and can be started or stopped at will.
9. Port Binding (port)
Impact: MEDIUM Description: Export services via port binding. The app is self-contained and does not rely on runtime injection of a webserver.
10. Dev/Prod Parity (parity)
Impact: MEDIUM Description: Keep development, staging, and production as similar as possible. Minimize gaps in time, personnel, and tools.
11. Logging (log)
Impact: MEDIUM Description: Treat logs as event streams. Write unbuffered to stdout, let the execution environment handle routing and storage.
12. Admin Processes (admin)
Impact: MEDIUM Description: Run admin/management tasks as one-off processes. Use the same codebase, config, and environment as regular processes.
Run Admin Tasks as One-Off Processes Not Special Scripts
Administrative tasks (database migrations, REPL sessions, data fixes) should run as one-off processes in an identical environment to the app's regular processes. They use the same codebase, config, and dependency isolation.
Incorrect (separate admin scripts):
# Admin scripts outside the main codebase
/scripts/migrate.sh # Different environment, different config
/scripts/fix_data.py # May have different dependencies
# SSH to production server and run manually
ssh prod-server
cd /var/www/app
source /different/virtualenv/bin/activate # Different env
python scripts/fix_data.py # Different config source
# What config did it use? What version of code?Correct (one-off processes from release):
# Admin commands are part of the app
# Django
python manage.py migrate
python manage.py shell
python manage.py fix_bad_records # Custom command
# Flask with Click
flask db upgrade
flask shell
flask fix-bad-records
# These run with the SAME:
# - Codebase version
# - Dependencies
# - Configuration
# - IsolationRunning admin processes in production:
# Heroku
heroku run python manage.py migrate
heroku run python manage.py shell
heroku run python scripts/fix_data.py
# Kubernetes
kubectl exec -it deployment/web -- python manage.py migrate
kubectl exec -it deployment/web -- python manage.py shell
# Or as a Job
kubectl apply -f - <<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: migration
spec:
template:
spec:
containers:
- name: migrate
image: myapp:v1.2.3 # Same image as web
command: ["python", "manage.py", "migrate"]
envFrom:
- secretRef:
name: app-secrets
restartPolicy: Never
EOF
# Docker
docker run --rm -e DATABASE_URL="..." myapp:v1.2.3 python manage.py migrateAdmin commands live in the codebase:
# myapp/management/commands/fix_bad_records.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Fix records with invalid status'
def add_arguments(self, parser):
parser.add_argument('--dry-run', action='store_true')
def handle(self, *args, **options):
records = Record.objects.filter(status='invalid')
self.stdout.write(f'Found {records.count()} invalid records')
if not options['dry_run']:
records.update(status='pending')
self.stdout.write(self.style.SUCCESS('Fixed!'))Benefits:
- Same code, same config, same dependencies
- Auditable: which release did the migration run against?
- Reproducible: can run same command in any environment
- Version controlled: admin scripts are part of the app
Reference: The Twelve-Factor App - Admin processes
Provide REPL Access for Debugging and Data Inspection
One-off admin processes include REPL (Read-Eval-Print Loop) sessions for interactive debugging and data inspection. The REPL runs against a release, giving developers access to the app's models and configuration in production.
Incorrect (ad-hoc REPL without proper environment):
# SSH to production server and run Python directly
ssh prod-server
cd /var/www/app
python
>>> import app # Wrong virtualenv, wrong config
>>> app.db.query("SELECT * FROM users")
# Uses wrong database, wrong dependencies
# No audit trail of what commands were runCorrect (REPL from the release environment):
# Django shell via kubectl - uses release's code and config
kubectl exec -it deployment/web -- python manage.py shell
# Flask shell
kubectl exec -it deployment/web -- flask shell
# Heroku - runs in isolated dyno with production config
heroku run python manage.py shell**Python REPL access:
# Django shell
kubectl exec -it deployment/web -- python manage.py shell
# Flask shell
kubectl exec -it deployment/web -- flask shell
# Generic Python with app context
kubectl exec -it deployment/web -- python
>>> from app import create_app, db
>>> app = create_app()
>>> with app.app_context():
... user = User.query.get(123)
... print(user.email)Ruby REPL access:
# Rails console
kubectl exec -it deployment/web -- bundle exec rails console
# IRB with app loaded
kubectl exec -it deployment/web -- bundle exec irb -r ./config/environmentNode.js REPL access:
# Node with app context
kubectl exec -it deployment/web -- node
> const db = require('./db')
> const User = require('./models/user')
> await User.findById(123)Safe REPL practices:
# Read-only investigation
>>> from app.models import Order
>>> order = Order.query.get(12345)
>>> print(order.status, order.items)
# CAREFUL with writes - use transactions
>>> from app import db
>>> with db.session.begin():
... order.status = 'cancelled'
... # Review change before commit
... print(f"Will update order {order.id} to cancelled")
... input("Press Enter to commit or Ctrl+C to abort")
# Better: use management commands for changes
# Instead of ad-hoc REPL writes
python manage.py cancel_order --order-id=12345 --dry-run
python manage.py cancel_order --order-id=12345Heroku style:
# One-off dyno with REPL
heroku run python manage.py shell
# Runs in isolated dyno with production config
# Changes to filesystem don't affect running app
# Safe sandbox for investigationRead-only replica for safety:
# Connect REPL to read replica for investigation
# settings.py
DATABASES = {
'default': os.environ['DATABASE_URL'],
'readonly': os.environ.get('DATABASE_READONLY_URL', os.environ['DATABASE_URL']),
}
# In REPL
>>> from django.db import connections
>>> with connections['readonly'].cursor() as cursor:
... cursor.execute("SELECT * FROM orders WHERE id = %s", [123])
... print(cursor.fetchone())
# No risk of accidental writesBenefits:
- Debug production issues with real data
- Inspect state without deploying logging
- Run one-off queries safely
- Test hypotheses interactively
Reference: The Twelve-Factor App - Admin processes
Run Admin Processes Against a Release with Same Codebase and Config
One-off admin processes must run against the same release as the app's regular processes. This means the same codebase version, the same config, and the same dependency isolation. Admin processes are NOT special - they're just short-lived processes of the same release.
Incorrect (admin with different environment):
# Developer laptop running migration against production
# Local code may be ahead/behind production
python manage.py migrate --database=$PROD_DATABASE_URL
# Danger: local code has unreleased migrations!
# Ops server with different code version
ssh ops-server
cd /var/www/app-v1.2.0 # Production is v1.2.3!
python manage.py fix_data
# Uses old code against new database schema
# Using different dependencies
pip install some-tool
python -c "import some_tool; some_tool.fix(db)"
# Not in requirements.txt, behavior differs from appCorrect (admin from the release):
# Run from the exact deployed release
# Kubernetes: use same image
kubectl run migration --rm -it \
--image=myapp:v1.2.3 \ # Same image as deployment
--restart=Never \
--env-from=secret/app-secrets \ # Same config
-- python manage.py migrate
# Heroku: runs against current release automatically
heroku run python manage.py migrate
# Uses current slug (release artifact) and config vars
# Docker: specify exact image
docker run --rm \
-e DATABASE_URL="$DATABASE_URL" \
myapp:v1.2.3 \
python manage.py migrateDependency isolation for admin:
# Python: use bundle exec equivalent
# Correct: uses app's virtualenv
kubectl exec deployment/web -- python manage.py shell
# Ruby: bundle exec ensures app's gems
kubectl exec deployment/web -- bundle exec rails console
# Node: uses app's node_modules
kubectl exec deployment/web -- npx ts-node scripts/fix_data.tsKubernetes Job pattern:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-v1.2.3
spec:
template:
spec:
containers:
- name: migrate
image: myapp:v1.2.3 # Exact release version
command: ["python", "manage.py", "migrate"]
envFrom:
- configMapRef:
name: app-config # Same config
- secretRef:
name: app-secrets # Same secrets
restartPolicy: Never
backoffLimit: 0Benefits:
- Migrations match the deployed code
- Admin scripts see same database schema assumptions
- No "works locally" vs "fails in production" issues
- Clear audit trail of what ran against what
Reference: The Twelve-Factor App - Admin processes
Generate One Build Artifact Per Commit Deploy Same Artifact Everywhere
Build the application once per commit, producing a single artifact that is deployed to all environments. Never rebuild for staging vs production - the artifact is identical, only configuration differs.
Incorrect (rebuild per environment):
# CI pipeline that rebuilds for each environment
deploy-staging:
script:
- npm install
- npm run build # Build #1 for staging
- deploy-to-staging
deploy-production:
script:
- npm install
- npm run build # Build #2 for production
- deploy-to-production
# Different npm install = potentially different packages
# Different build = potentially different output
# "Works in staging" doesn't guarantee production worksCorrect (build once, deploy anywhere):
# CI pipeline with single build
stages:
- build
- deploy-staging
- deploy-production
build:
stage: build
script:
- npm ci # Deterministic install from lockfile
- npm run build
- docker build -t myapp:$CI_COMMIT_SHA .
- docker push registry/myapp:$CI_COMMIT_SHA
artifacts:
# Build artifact available to later stages
paths:
- dist/
deploy-staging:
stage: deploy-staging
script:
# Deploy the SAME artifact built above
- kubectl set image deployment/myapp app=registry/myapp:$CI_COMMIT_SHA
# Staging uses staging config
environment: staging
deploy-production:
stage: deploy-production
script:
# Deploy the EXACT SAME artifact
- kubectl set image deployment/myapp app=registry/myapp:$CI_COMMIT_SHA
# Production uses production config
environment: production
when: manual # Requires approvalContainer tagging strategy:
# Tag with commit SHA (immutable)
docker build -t myapp:abc123def .
docker push registry/myapp:abc123def
# Optionally add semantic tags to same image
docker tag myapp:abc123def myapp:v1.2.3
docker tag myapp:abc123def myapp:latest
docker push registry/myapp:v1.2.3
docker push registry/myapp:latest
# All tags point to identical image
# Staging deployed with :abc123def
# Production deployed with same :abc123defBenefits:
- What passed tests in staging is exactly what runs in production
- No "but the production build is different" debugging
- Artifact can be promoted without rebuilding
- Audit trail: commit SHA → artifact → all deployments
Reference: The Twelve-Factor App - Build, Release, Run
Push Complexity Into Build Stage Keep Run Stage Minimal
The run stage should have as few moving parts as possible. Complex operations like compilation, asset bundling, dependency resolution, and code generation happen in the build stage. The run stage simply starts processes from a pre-built artifact.
Incorrect (complexity at runtime):
# Dockerfile that does too much at runtime
FROM python:3.11
WORKDIR /app
COPY . .
# This happens at container start
CMD pip install -r requirements.txt && \
python manage.py migrate && \
python manage.py collectstatic && \
python manage.py compilemessages && \
gunicorn app:application
# If pip install fails at 3AM, app doesn't start
# Migration failure brings down the whole deploy
# Slow startup, unpredictable timingCorrect (complexity in build stage):
# Multi-stage build - complexity in build
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
RUN python manage.py compilemessages
# Runtime stage - minimal
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app /app
ENV PATH=/root/.local/bin:$PATH
# Simple, fast startup
CMD ["gunicorn", "app:application", "--bind", "0.0.0.0:8000"]# Migrations run separately, not at startup
# Either as a release task or admin process
kubectl exec -it deployment/myapp -- python manage.py migrate
# Or in CI/CD pipeline before deployment completesWhat belongs in build stage:
- Dependency installation
- Compilation (TypeScript, Sass, etc.)
- Asset bundling and minification
- Static file collection
- Translation compilation
- Code generation
What belongs in run stage:
- Starting the application process
- Reading environment config
- Binding to port
Benefits:
- App starts in seconds, not minutes
- Failures happen in CI, not at 3AM restart
- Crashed processes can restart immediately
- Horizontal scaling is fast (new instances ready quickly)
Reference: The Twelve-Factor App - Build, Release, Run
Create Immutable Releases with Unique Identifiers
Every release must have a unique identifier (timestamp or incrementing number) and be immutable once created. A release is a specific build combined with specific config - any change requires a new release. Releases are append-only; you never modify an existing release.
Incorrect (mutable releases):
# "Release" is just the latest code on the server
ssh prod-server
cd /var/www/app
# Hotfix applied directly
vim app.py # Quick fix for urgent bug
systemctl restart app
# Config changed in place
echo "NEW_FEATURE=true" >> .env
systemctl restart app
# What's actually running? Combination of:
# - Some git commit
# - Plus manual edits
# - Plus config accumulated over time
# - Rollback? Impossible.Correct (immutable releases):
# Each release is a frozen snapshot
releases/
├── v100/
│ ├── build/ # From build stage
│ ├── config.json # Frozen config snapshot
│ └── RELEASE_INFO # Metadata
├── v101/
│ ├── build/
│ ├── config.json
│ └── RELEASE_INFO
└── v102/ # Current
├── build/
├── config.json
└── RELEASE_INFO
# Symlink points to current release
current -> v102
# Rollback is trivial
ln -sfn releases/v101 current
systemctl restart app# RELEASE_INFO example
release_id: v102
created_at: 2024-01-15T14:30:00Z
build_sha: abc123def456
deployed_by: ci-pipeline
config_hash: sha256:789xyzContainer-based releases:
# Each image tag is an immutable release
docker images
# REPOSITORY TAG CREATED
# myapp v102 2 hours ago
# myapp v101 1 day ago
# myapp v100 3 days ago
# Deploy specific release
kubectl set image deployment/myapp app=myapp:v102
# Rollback to previous
kubectl rollout undo deployment/myapp
# Or explicit: kubectl set image deployment/myapp app=myapp:v101Benefits:
- Audit trail: know exactly what ran when
- Instant rollback: no rebuild required
- Reproducibility: redeploy same release to new environment
- Debugging: reproduce exact release state
Reference: The Twelve-Factor App - Build, Release, Run
Never Modify Code at Runtime - Changes Require New Release
Code changes cannot be made at runtime because there is no way to propagate those changes back to the build stage. Any fix, feature, or update requires going through the full build-release-run pipeline. This ensures every change is tracked, tested, and reproducible.
Incorrect (runtime modifications):
# Production server
ssh prod-server
cd /var/www/app
# "Quick fix" - edit code directly
vim app/views.py
# Add a try/except to handle edge case
# "Quick config" - change settings file
vim config/settings.py
# TIMEOUT = 30 # was 10
# "Quick update" - pull specific file
git checkout origin/main -- app/utils.py
systemctl restart app
# These changes exist only on this server
# Not in git, not reproducible, no tests ranCorrect (all changes via build pipeline):
# 1. Make change in development
git checkout -b fix/handle-edge-case
vim app/views.py
git add -A && git commit -m "Handle edge case in view"
# 2. Push triggers CI pipeline
git push origin fix/handle-edge-case
# CI runs tests, linting, security scans
# 3. Merge after review
# PR merged to main
# 4. Build stage creates new artifact
# Build: myapp-abc123.tar.gz
# 5. Release stage creates new release
# Release: v103
# 6. Run stage deploys new release
kubectl set image deployment/myapp app=myapp:v103
# Change is tracked, tested, reproducible
git log --oneline
# abc123 Handle edge case in viewEmergency hotfix process:
# Even urgent fixes go through the pipeline
# Just with an expedited process
# 1. Branch from production tag
git checkout v102 -b hotfix/urgent-fix
vim app/views.py
git commit -m "Hotfix: handle null case"
# 2. Emergency CI run (maybe skip some slow tests)
git push origin hotfix/urgent-fix
# CI: lint, unit tests, security (skip e2e)
# 3. Fast-track merge and deploy
# Release: v103-hotfix
kubectl set image deployment/myapp app=myapp:v103-hotfix
# 4. Cherry-pick to main after incident
git checkout main
git cherry-pick abc123Benefits:
- Every change is in version control
- All changes are tested before deployment
- Any server can be rebuilt from scratch
- Rollback is always possible
Reference: The Twelve-Factor App - Build, Release, Run
Strictly Separate Build, Release, and Run Stages
A twelve-factor app has three distinct stages: build (compile code into executable), release (combine build with config), and run (launch processes). These stages are strictly separated - you cannot modify code at runtime, and each release is immutable.
Incorrect (blurred stages):
# SSH into production server
ssh prod-server
# Pull latest code directly on production
cd /var/www/app
git pull origin main
# Install dependencies on production
pip install -r requirements.txt
# Modify config file on server
vim config.py
# Restart
systemctl restart app
# No build artifact, no release tracking
# "What version is running?" - who knowsCorrect (separate stages):
# BUILD STAGE (CI server)
# Triggered by git push, produces an artifact
git checkout $COMMIT_SHA
pip install -r requirements.txt
python -m compileall .
tar -czf build-${COMMIT_SHA}.tar.gz .
aws s3 cp build-${COMMIT_SHA}.tar.gz s3://builds/
# RELEASE STAGE (deployment)
# Combines build artifact with environment config
# Creates immutable release v42
release_id="v42"
aws s3 cp s3://builds/build-${COMMIT_SHA}.tar.gz ./
# Config comes from environment, not bundled
# RUN STAGE (execution environment)
# Starts processes from the release
DATABASE_URL="..." python app.py
# Code cannot be modified hereUsing containers (clear separation):
# BUILD STAGE
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .
RUN python -m compileall .
# RELEASE: image tagged with commit SHA
# docker build -t myapp:abc123 .
# docker push registry/myapp:abc123
# RUN: container started with config
# docker run -e DATABASE_URL="..." registry/myapp:abc123Benefits:
- Build once, deploy many times (same artifact to staging and production)
- Releases are numbered and trackable
- Rollback = redeploy previous release
- No "it worked when I deployed it manually" issues
Reference: The Twelve-Factor App - Build, Release, Run
Use Deploys Not Branches to Represent Environments
Different environments (dev, staging, production) should be deploys of the same codebase at different commits, not separate branches with environment-specific code. The codebase is identical; only configuration differs.
Incorrect (environment-specific branches):
# Branches diverge with environment-specific changes
git branch -a
# * main
# staging # Has staging-specific hacks
# production # Has production-specific hacks
# Merging becomes a nightmare
git checkout production
git merge main
# CONFLICT: config/database.js has production-specific settings
# Code contains environment conditionals
if (process.env.NODE_ENV === 'production') {
// Production-only code path
enableCaching();
} else if (process.env.NODE_ENV === 'staging') {
// Staging-only code path
enableDebugLogging();
}Correct (single codebase, multiple deploys):
# One main branch, tags mark releases
git log --oneline --decorate
# a1b2c3d (HEAD -> main) Latest feature
# d4e5f6g (tag: v1.2.3) Production release
# h7i8j9k (tag: v1.2.2) Previous release
# Deploy specific versions to environments
# Production: v1.2.3
# Staging: main (latest)
# Dev: local checkout// Code is identical across all environments
// Behavior varies only through configuration
const cacheEnabled = process.env.CACHE_ENABLED === 'true';
const logLevel = process.env.LOG_LEVEL || 'info';
if (cacheEnabled) {
enableCaching();
}
logger.setLevel(logLevel);Benefits:
- No merge conflicts between environment branches
- Every commit can be deployed to any environment
- Staging truly tests what will go to production
Reference: The Twelve-Factor App - Codebase
Enforce One-to-One Correlation Between Codebase and Application
Each application must have exactly one codebase. If you have multiple codebases, you have a distributed system where each component is its own app. If multiple apps share the same code, factor out the shared functionality into libraries.
Incorrect (multiple apps sharing one codebase):
my-monorepo/
├── frontend-app/
│ └── package.json # App 1
├── backend-api/
│ └── package.json # App 2
├── worker-service/
│ └── package.json # App 3
└── shared/
└── utils.js # Shared code tightly coupled
# All three apps deployed together, can't scale independentlyCorrect (separate codebases with shared libraries):
# Each app has its own repository
github.com/company/frontend-app # App 1
github.com/company/backend-api # App 2
github.com/company/worker-service # App 3
github.com/company/shared-utils # Published as npm/pip package
# package.json in frontend-app
{
"dependencies": {
"@company/shared-utils": "^2.1.0"
}
}
# Each app can be deployed, scaled, and versioned independentlyWhen NOT to use this pattern:
- During initial prototyping when boundaries are unclear
- Monorepos are acceptable if each app has independent build/deploy pipelines
Benefits:
- Each app can be deployed independently
- Different apps can use different versions of shared code
- Teams can work in parallel without blocking each other
Reference: The Twelve-Factor App - Codebase
Factor Shared Code Into Libraries Managed by Dependency Manager
When multiple applications need the same functionality, extract it into a library that is included through the dependency manager. This maintains the one-codebase-per-app rule while enabling code reuse.
Incorrect (copy-pasting shared code):
# Duplicated code across apps
app-1/
├── src/
│ ├── utils/
│ │ └── validation.js # Copy-pasted
│ └── index.js
app-2/
├── src/
│ ├── utils/
│ │ └── validation.js # Same code, may drift
│ └── index.js
# Bug fixes must be applied in multiple placesCorrect (shared library via package manager):
# Shared library published to package registry
@company/validation/
├── package.json
│ {
│ "name": "@company/validation",
│ "version": "1.3.0"
│ }
├── src/
│ └── index.js
└── README.md// app-1/package.json
{
"name": "app-1",
"dependencies": {
"@company/validation": "^1.3.0"
}
}// app-2/package.json
{
"name": "app-2",
"dependencies": {
"@company/validation": "^1.3.0"
}
}// Both apps import from the library
import { validateEmail, validatePhone } from '@company/validation';Benefits:
- Single source of truth for shared functionality
- Version pinning allows apps to upgrade independently
- Bug fixes are released once, consumed via version bump
- Clear ownership and maintenance boundaries
Reference: The Twelve-Factor App - Codebase
Maintain One Codebase Per Application in Version Control
Every twelve-factor application has exactly one codebase tracked in version control. This single source of truth ensures all deploys (dev, staging, production) originate from the same code, eliminating "works on my machine" issues and enabling reliable rollbacks.
Incorrect (multiple codebases or no version control):
# Multiple separate directories for each environment
/app-dev/
/app-staging/
/app-production/
# Or copying code between machines via FTP/SCP
scp -r ./myapp user@prod:/var/www/
# No version history, no rollback capabilityCorrect (single codebase with version control):
# One repository, many deploys
git clone https://github.com/company/myapp.git
# Different environments use the same codebase at different commits
# Production: deployed from tag v1.2.3
# Staging: deployed from main branch
# Development: local checkout of main branch
git log --oneline
# a1b2c3d (HEAD -> main, tag: v1.2.3) Fix payment processing
# d4e5f6g Add user authentication
# h7i8j9k Initial commitBenefits:
- Complete history of all changes enables bisecting bugs
- Any developer can reproduce any deploy
- Rollbacks are trivial: just redeploy an earlier commit
- Code reviews and CI/CD operate on the single source of truth
Reference: The Twelve-Factor App - Codebase
Never Commit Secrets or Credentials to Version Control
Secrets committed to git remain in the repository history forever, even after deletion. A twelve-factor app never contains credentials in its codebase. Use environment variables, secret managers, or encrypted secret files that are explicitly gitignored.
Incorrect (secrets in code or config files):
# config.py - COMMITTED TO GIT
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
STRIPE_KEY = "sk_live_abc123"# docker-compose.yml - COMMITTED TO GIT
services:
app:
environment:
- DATABASE_PASSWORD=supersecret123# .env file committed to git (common mistake)
API_KEY=secret_valueCorrect (secrets from external sources):
# config.py - safe to commit
import os
AWS_ACCESS_KEY = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
STRIPE_KEY = os.environ["STRIPE_API_KEY"]# .gitignore - prevent accidental commits
.env
.env.*
*.pem
*.key
secrets/# docker-compose.yml - references external secrets
services:
app:
env_file:
- .env # Not committed, each developer creates their own
secrets:
- db_password
secrets:
db_password:
external: true # Managed by Docker Swarm or similarIf you accidentally committed a secret:
# 1. Rotate the credential IMMEDIATELY (it's compromised)
# 2. Remove from current code
# 3. Clean git history (complex, may require force push)
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch path/to/secret" \
--prune-empty --tag-name-filter cat -- --all
# 4. Still assume it's compromised - history might be cached/forkedBenefits:
- Repository can be safely open-sourced
- Credentials can be rotated without code changes
- Security audit is straightforward
Reference: The Twelve-Factor App - Config
Treat Environment Variables as Granular Controls Not Grouped Environments
A twelve-factor app never groups config into named "environments" like development, staging, production. Instead, each environment variable is an independent control that can be set differently for each deploy. This scales cleanly as deploys multiply.
Incorrect (grouped environments):
# settings.py
ENVIRONMENTS = {
'development': {
'DEBUG': True,
'DATABASE_URL': 'sqlite:///dev.db',
'LOG_LEVEL': 'DEBUG',
},
'staging': {
'DEBUG': False,
'DATABASE_URL': 'postgresql://staging-db/app',
'LOG_LEVEL': 'INFO',
},
'production': {
'DEBUG': False,
'DATABASE_URL': 'postgresql://prod-db/app',
'LOG_LEVEL': 'WARNING',
},
# Need a new QA environment? Add 'qa' here.
# Need joes-staging? Add 'joes_staging' here.
# Combinatorial explosion!
}
env = os.environ.get('ENVIRONMENT', 'development')
config = ENVIRONMENTS[env]Correct (granular, independent env vars):
# settings.py
import os
# Each setting is independent
DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'
DATABASE_URL = os.environ['DATABASE_URL']
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
CACHE_TTL = int(os.environ.get('CACHE_TTL', '3600'))
FEATURE_NEW_UI = os.environ.get('FEATURE_NEW_UI', 'false').lower() == 'true'
# No predefined environments
# Each deploy sets exactly the values it needs# Production
DATABASE_URL="postgresql://prod/app"
LOG_LEVEL="WARNING"
DEBUG="false"
# Staging
DATABASE_URL="postgresql://staging/app"
LOG_LEVEL="INFO"
DEBUG="false"
# Joe's personal staging
DATABASE_URL="postgresql://joe-staging/app"
LOG_LEVEL="DEBUG"
DEBUG="true"
FEATURE_NEW_UI="true"
# No code changes needed for new deploysBenefits:
- Adding a new deploy requires zero code changes
- Each deploy can have unique configuration
- No implicit coupling between unrelated settings
Reference: The Twelve-Factor App - Config
Strictly Separate Configuration from Code
A twelve-factor app strictly separates config from code. Configuration is anything likely to vary between deploys (database URLs, credentials, feature flags). A litmus test: could you open-source your codebase right now without exposing credentials?
Incorrect (config hardcoded in code):
# settings.py
DATABASE_URL = "postgresql://admin:secretpass@prod-db.company.com:5432/myapp"
STRIPE_API_KEY = "sk_live_abc123xyz"
DEBUG = False
# This file is committed to git
# Anyone with repo access sees production credentials
# Deploying to staging requires code changesCorrect (config externalized):
# settings.py
import os
DATABASE_URL = os.environ["DATABASE_URL"]
STRIPE_API_KEY = os.environ["STRIPE_API_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
# Code is safe to open-source
# Each deploy sets its own environment variables# Development
export DATABASE_URL="postgresql://dev:dev@localhost:5432/myapp_dev"
export STRIPE_API_KEY="sk_test_xxx"
export DEBUG="true"
# Production (set by deployment platform)
DATABASE_URL="postgresql://admin:xxx@prod-db:5432/myapp"
STRIPE_API_KEY="sk_live_xxx"
DEBUG="false"What IS configuration (externalize):
- Database connection strings
- Credentials for external services (APIs, S3, etc.)
- Per-deploy values (hostnames, feature flags)
What is NOT configuration (keep in code):
- Internal routing (e.g.,
config/routes.rb) - Dependency injection wiring
- Code structure decisions
Reference: The Twelve-Factor App - Config
Store Configuration in Environment Variables
The twelve-factor app stores config in environment variables. Env vars are easy to change between deploys without code changes, unlikely to be accidentally committed to the repo, and are a language-agnostic standard supported by all operating systems and cloud platforms.
Incorrect (config files not in version control):
# config/database.yml - not committed to git but...
production:
host: prod-db.company.com
password: secretpass
# Problems:
# 1. Easy to accidentally commit (one wrong .gitignore)
# 2. Different format per framework (YAML, JSON, INI, TOML)
# 3. Where does this file come from on fresh deploy?
# 4. How do you manage it across 50 servers?Correct (environment variables):
import os
# Works identically regardless of deployment platform
database_url = os.environ["DATABASE_URL"]
redis_url = os.environ["REDIS_URL"]
api_key = os.environ["API_KEY"]# Set in shell
export DATABASE_URL="postgresql://user:pass@host/db"
# Or in Docker
docker run -e DATABASE_URL="..." myapp
# Or in Kubernetes
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
# Or in Heroku/Railway/Render dashboard
# Or in AWS Systems Manager Parameter Store
# Or in GitHub Actions secretsBenefits:
- Every platform supports environment variables
- Credentials never touch the filesystem
- Easy to rotate: change the env var, restart the app
- Works with secret management tools (Vault, AWS Secrets Manager)
Reference: The Twelve-Factor App - Config
Validate Required Configuration at Application Startup
A twelve-factor app fails fast and loudly if required configuration is missing or invalid. Check all required environment variables at startup, before accepting traffic, to avoid discovering configuration problems at 3 AM when a code path finally tries to use the missing value.
Incorrect (late configuration failure):
import os
# No validation at startup
DATABASE_URL = os.environ.get('DATABASE_URL') # Might be None
def get_user(user_id):
# Fails here, possibly hours after startup
# during a critical user request
conn = connect(DATABASE_URL) # TypeError: expected string, got None
return conn.execute("SELECT * FROM users WHERE id = ?", user_id)Correct (early validation):
import os
import sys
class Config:
def __init__(self):
self.database_url = self._require('DATABASE_URL')
self.redis_url = self._require('REDIS_URL')
self.api_key = self._require('API_KEY')
self.log_level = os.environ.get('LOG_LEVEL', 'INFO')
self.debug = os.environ.get('DEBUG', 'false').lower() == 'true'
# Validate format
if not self.database_url.startswith(('postgresql://', 'mysql://')):
self._fail('DATABASE_URL must be a valid database connection string')
def _require(self, name):
value = os.environ.get(name)
if not value:
self._fail(f'Required environment variable {name} is not set')
return value
def _fail(self, message):
print(f'Configuration error: {message}', file=sys.stderr)
sys.exit(1)
# Validate immediately at import time
config = Config()
# App only starts if all config is valid
def get_user(user_id):
conn = connect(config.database_url) # Always valid
return conn.execute("SELECT * FROM users WHERE id = ?", user_id)Benefits:
- App fails immediately with a clear error message
- Deployment fails before routing traffic to broken instance
- Easier debugging: error message names the missing variable
- Prevents partial startup where some features work and others don't
Reference: The Twelve-Factor App - Config
Use Lockfiles for Deterministic Dependency Resolution
While a manifest declares acceptable version ranges, a lockfile pins exact versions including all transitive dependencies. This ensures that pip install today produces identical results to pip install six months from now.
Incorrect (version ranges without lockfile):
# pyproject.toml - only specifies ranges
[project]
dependencies = [
"requests>=2.28.0",
"boto3>=1.26.0",
]
# Today: installs requests 2.31.0, boto3 1.34.0
# Next month: installs requests 2.32.0, boto3 1.35.0
# Subtle breaking changes cause production bugsCorrect (lockfile pins exact versions):
# Generate lockfile with pip-compile (pip-tools)
pip-compile pyproject.toml -o requirements.lock
# Or use poetry
poetry lock# requirements.lock (generated)
requests==2.31.0
# via myapp (pyproject.toml)
urllib3==2.1.0
# via requests
certifi==2023.11.17
# via requests
boto3==1.34.14
# via myapp (pyproject.toml)
botocore==1.34.14
# via boto3
# Every transitive dependency pinned# Install from lockfile for reproducible builds
pip install -r requirements.lock
# CI/CD uses the same lockfile
# Production uses the same lockfile
# All environments are identicalWhen to update the lockfile:
- When intentionally upgrading dependencies
- When adding new dependencies
- As part of regular security update cycles
Benefits:
- Yesterday's build == Today's build == Tomorrow's build
- Security patches can be precisely tracked
- Debugging is easier when you know exact versions
- Rollbacks restore exact dependency state
Reference: The Twelve-Factor App - Dependencies
Declare All Dependencies Explicitly in a Manifest File
A twelve-factor app declares all dependencies completely and exactly via a dependency declaration manifest. This includes direct dependencies and their transitive dependencies, ensuring any developer can build and run the application with just the language runtime and dependency manager installed.
Incorrect (implicit dependencies):
# No requirements.txt or pyproject.toml
# Developer assumes packages are globally installed
# app.py
import requests # Assumed to be installed globally
import numpy # Version? Who knows
import pandas # Might work with pandas 1.x or 2.x
# README says "install the usual stuff"
# New developer: "What's the usual stuff?"Correct (explicit dependency manifest):
# pyproject.toml
[project]
name = "myapp"
version = "1.0.0"
dependencies = [
"requests>=2.28.0,<3.0.0",
"numpy>=1.24.0,<2.0.0",
"pandas>=2.0.0,<3.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=23.0.0",
]# New developer setup is deterministic
git clone https://github.com/company/myapp.git
cd myapp
pip install -e .
# All dependencies installed at compatible versionsAlternative (lockfile for exact reproducibility):
# requirements.txt with pinned versions from pip-compile
requests==2.31.0
numpy==1.26.2
pandas==2.1.3
# Every build uses identical dependency versionsBenefits:
- New developers can build immediately after clone
- CI/CD produces identical builds to development
- Security audits can enumerate all dependencies
- Version conflicts are detected early
Reference: The Twelve-Factor App - Dependencies
Isolate Dependencies to Prevent System Package Leakage
A twelve-factor app uses a dependency isolation tool during execution to ensure no implicit dependencies "leak in" from the surrounding system. This guarantees the app runs identically regardless of what packages are installed globally.
Incorrect (relying on system packages):
# Globally installed packages pollute the environment
sudo pip install requests==2.25.0 # System-wide
# App expects requests 2.31.0 but gets 2.25.0
python app.py
# ImportError: cannot import name 'JSONDecodeError' from 'requests'
# Another app needs requests 2.20.0 - conflict!Correct (isolated environment):
# Python: Use virtual environments
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# All packages installed in .venv/, isolated from system
# Node.js: node_modules provides isolation by default
npm install
# All packages in ./node_modules/
# Ruby: Use bundler with bundle exec
bundle install --path vendor/bundle
bundle exec ruby app.rbAlternative (containerized isolation):
# Dockerfile provides complete isolation
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# Container has only declared dependencies, nothing from hostBenefits:
- App behavior is identical across developer machines, CI, and production
- Multiple apps on the same machine can use different dependency versions
- Upgrading system packages never breaks existing apps
- Security vulnerabilities in system packages don't affect isolated apps
Reference: The Twelve-Factor App - Dependencies
Never Rely on Implicit System Tools Being Available
A twelve-factor app does not rely on the implicit existence of any system tools. While tools like ImageMagick, curl, or ffmpeg may exist on many systems, there is no guarantee they will be available in production or that their versions will be compatible.
Incorrect (assuming system tools exist):
import subprocess
def resize_image(input_path, output_path):
# Assumes ImageMagick is installed
subprocess.run([
'convert', input_path,
'-resize', '800x600',
output_path
])
# Fails silently or crashes if convert isn't installed
# Version differences cause unexpected behavior
def fetch_data(url):
# Assumes curl is installed
result = subprocess.run(
['curl', '-s', url],
capture_output=True
)
return result.stdoutCorrect (use language libraries or vendor tools):
# Use a library instead of shelling out
from PIL import Image
import requests
def resize_image(input_path, output_path):
# Pillow is declared in requirements.txt
with Image.open(input_path) as img:
img.thumbnail((800, 600))
img.save(output_path)
def fetch_data(url):
# requests is declared in requirements.txt
response = requests.get(url)
return response.contentAlternative (vendor the tool):
# If you must use a system tool, make it explicit
FROM python:3.11-slim
# Explicitly install required system tools
RUN apt-get update && apt-get install -y \
imagemagick \
&& rm -rf /var/lib/apt/lists/*
# Or better: use a base image that includes the tool
FROM dpokidov/imagemagick:latestBenefits:
- App runs on any system with the language runtime installed
- No surprises when deploying to new infrastructure
- Dependency on system tools is explicit in Dockerfile or documented
Reference: The Twelve-Factor App - Dependencies
Design for Crash-Only Software That Recovers from Sudden Death
Even with graceful shutdown, processes can die suddenly (hardware failure, OOM killer, network partition). Design your application to recover cleanly from abrupt termination without data loss or corruption.
Incorrect (assumes clean shutdown):
class JobProcessor:
def process_job(self, job):
# Mark job as "in progress" in database
db.execute("UPDATE jobs SET status='processing' WHERE id=?", job.id)
# Do the work
result = expensive_computation(job.data)
# Save result and mark complete
save_result(result)
db.execute("UPDATE jobs SET status='complete' WHERE id=?", job.id)
# If process dies after marking "processing" but before "complete":
# Job stuck in "processing" forever
# Other workers won't pick it up
# Manual intervention requiredCorrect (crash-safe design):
class JobProcessor:
def process_job(self, job):
# Idempotent processing with timeout
# Other workers can retry if we die
lock = redis.lock(f'job:{job.id}', timeout=300) # 5 min max
if not lock.acquire(blocking=False):
return # Another worker has it
try:
# Check if already completed (idempotency)
if db.query("SELECT status FROM jobs WHERE id=?", job.id) == 'complete':
return
result = expensive_computation(job.data)
# Atomic transaction: save result AND mark complete
with db.transaction():
save_result(result)
db.execute("UPDATE jobs SET status='complete' WHERE id=?", job.id)
finally:
lock.release()
# If process dies:
# - Lock expires after 5 minutes
# - Another worker picks up the job
# - Idempotency check prevents duplicate processingQueue-based crash safety:
from celery import Celery
app = Celery('tasks')
app.conf.task_acks_late = True # Acknowledge AFTER completion
app.conf.task_reject_on_worker_lost = True # Requeue if worker dies
@app.task(bind=True, max_retries=3)
def process_job(self, job_id):
try:
job = get_job(job_id)
result = process(job)
save_result(result)
except Exception as e:
# On any failure, task returns to queue
raise self.retry(exc=e, countdown=60)
# Only acknowledged after successful return
# Worker death = task returned to queue automaticallyDatabase transaction safety:
# Use transactions for multi-step operations
def transfer_funds(from_account, to_account, amount):
with db.transaction():
db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
amount, from_account)
db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, to_account)
# Either both succeed or both fail
# Crash mid-transaction = automatic rollbackBenefits:
- Hardware failures don't lose data
- Automatic recovery without manual intervention
- Simpler operations - just restart everything
- Confidence in rapid deployment and scaling
Reference: The Twelve-Factor App - Disposability
Design Processes to Be Disposable Started or Stopped at Any Moment
Twelve-factor app processes are disposable - they can be started or stopped at a moment's notice. This supports fast elastic scaling, rapid deployment of code or config changes, and robustness of production deploys.
Incorrect (processes that can't be stopped):
# Process assumes it will run forever
class LongRunningProcessor:
def __init__(self):
self.running = True
def run(self):
while self.running:
# Hours-long batch job
data = fetch_all_records() # 1 million records
for record in data:
process_record(record)
# Can't stop mid-batch
# Scaling down kills in-progress work
# Deploy must wait for batch to completeCorrect (disposable, interruptible processes):
import signal
import sys
class DisposableProcessor:
def __init__(self):
self.should_stop = False
# Handle shutdown signals gracefully
signal.signal(signal.SIGTERM, self._handle_shutdown)
signal.signal(signal.SIGINT, self._handle_shutdown)
def _handle_shutdown(self, signum, frame):
print("Shutdown signal received, finishing current item...")
self.should_stop = True
def run(self):
while not self.should_stop:
# Process one item at a time
record = fetch_one_record()
if record:
process_record(record)
mark_complete(record)
else:
# No work, check again soon
time.sleep(1)
print("Graceful shutdown complete")
sys.exit(0)
# Can be stopped anytime - at most loses one record
# Which is immediately reprocessed by another workerKubernetes graceful shutdown:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 30 # Time to finish
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
# SIGTERM sent, then 30s to finish, then SIGKILLBenefits:
- New deployments can start immediately
- Scale down doesn't lose work
- Crashed processes quickly replaced
- Autoscaling responds to demand in seconds
Reference: The Twelve-Factor App - Disposability
Minimize Startup Time to Enable Rapid Scaling and Recovery
Processes should start in seconds, not minutes. Fast startup enables rapid scaling in response to traffic spikes and quick recovery from process crashes. Slow startup creates vulnerability windows and delays deployments.
Incorrect (slow startup):
# Startup does too much
def create_app():
app = Flask(__name__)
# Load entire ML model into memory - 30 seconds
app.model = load_large_model('model.pkl')
# Pre-warm caches with ALL data - 45 seconds
app.cache = {}
for item in fetch_all_items(): # 100k items
app.cache[item.id] = item
# Verify all external services - 15 seconds
verify_database_connection()
verify_redis_connection()
verify_s3_connection()
verify_email_service()
return app
# Total: 90 seconds before handling first request
# Autoscaler adds capacity too slowly during traffic spikeCorrect (fast startup):
def create_app():
app = Flask(__name__)
# Lazy-load expensive resources
app.model = None # Loaded on first use
@app.before_first_request
def lazy_init():
# Model loaded after app is ready for traffic
# First request waits, subsequent requests don't
if app.model is None:
app.model = load_large_model('model.pkl')
# Use external cache, don't pre-warm in process
app.redis = redis.from_url(os.environ['REDIS_URL'])
# Health check without verifying all dependencies
@app.route('/health')
def health():
return {'status': 'ok'}
return app
# Startup: <5 seconds
# Ready to receive health checks immediatelyTechniques for fast startup:
# 1. Lazy loading
class LazyModel:
def __init__(self, path):
self._path = path
self._model = None
@property
def model(self):
if self._model is None:
self._model = load_model(self._path)
return self._model
# 2. Background initialization
import threading
def init_cache_background():
"""Run after app starts, doesn't block startup"""
time.sleep(5) # Wait for app to be stable
warm_cache()
threading.Thread(target=init_cache_background, daemon=True).start()
# 3. Pre-built artifacts
# Move compilation to build stage, not runtime
# See: build-complexity-in-build.mdKubernetes readiness probe:
spec:
containers:
- name: app
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 2 # Start checking quickly
periodSeconds: 5
# Traffic routes only after readiness passes
# Fast startup = fast traffic routingBenefits:
- Autoscaler adds capacity within seconds of traffic spike
- Crashed processes replaced immediately
- Rolling deployments complete faster
- Better resource utilization
Reference: The Twelve-Factor App - Disposability
Implement Graceful Shutdown on SIGTERM
When a process receives SIGTERM, it should stop accepting new work, finish in-flight requests, then exit cleanly. This enables zero-downtime deployments and prevents data loss during scaling operations.
Incorrect (abrupt termination):
# No signal handling - killed immediately
from flask import Flask
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process():
# 30-second operation
data = request.json
result = expensive_operation(data) # Halfway through...
save_result(result) # SIGKILL - never saved!
return {'result': result}
# On SIGTERM: process killed mid-request
# Client gets connection reset
# Data potentially corruptedCorrect (graceful shutdown):
import signal
import sys
from flask import Flask
from werkzeug.serving import make_server
app = Flask(__name__)
server = None
@app.route('/process', methods=['POST'])
def process():
data = request.json
result = expensive_operation(data)
save_result(result)
return {'result': result}
def handle_sigterm(signum, frame):
print("SIGTERM received, shutting down gracefully...")
if server:
server.shutdown() # Stop accepting new connections
# Register signal handler
signal.signal(signal.SIGTERM, handle_sigterm)
if __name__ == '__main__':
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
# After shutdown(), in-flight requests complete
# Then process exits cleanlyProduction WSGI server with graceful shutdown:
# gunicorn.conf.py
graceful_timeout = 30 # Seconds to wait for workers to finish
timeout = 30 # Request timeout
def on_exit(server):
print("Gunicorn shutting down gracefully")
def worker_exit(server, worker):
print(f"Worker {worker.pid} exiting")# Start with config
gunicorn app:app -c gunicorn.conf.py
# On SIGTERM:
# 1. Stop accepting new connections
# 2. Wait up to graceful_timeout for in-flight requests
# 3. Exit cleanlyWorker graceful shutdown:
from celery import Celery
from celery.signals import worker_shutting_down
app = Celery('tasks')
@worker_shutting_down.connect
def handle_shutdown(sig, how, exitcode, **kwargs):
print("Worker shutting down, completing current task...")
# Current task will complete
# No new tasks accepted
@app.task(acks_late=True) # Acknowledge after completion
def process_job(data):
result = expensive_operation(data)
save_result(result)
return result
# If killed before completion, job returns to queueBenefits:
- Zero-downtime deployments
- No dropped requests during scaling
- Data integrity preserved
- Clean resource cleanup (connections, file handles)
Reference: The Twelve-Factor App - Disposability
Make Operations Idempotent to Safely Retry After Failures
Operations should be safe to retry without side effects. If a process dies mid-operation and the operation is retried, the end result should be the same as if it succeeded once. This is critical for crash recovery and queue-based processing.
Incorrect (non-idempotent operations):
@app.task
def send_welcome_email(user_id):
user = get_user(user_id)
send_email(user.email, "Welcome!")
# If worker dies after sending but before ack:
# Task retried = user gets 2 emails
@app.task
def charge_customer(order_id, amount):
order = get_order(order_id)
stripe.Charge.create(amount=amount, customer=order.customer_id)
order.status = 'paid'
order.save()
# If dies after charging but before save:
# Retry = customer charged twice!
@app.task
def process_inventory(item_id, quantity):
item = get_item(item_id)
item.stock -= quantity # Decrement
item.save()
# Retry = double decrementCorrect (idempotent operations):
@app.task
def send_welcome_email(user_id):
user = get_user(user_id)
# Check if already sent (idempotency key in database)
if user.welcome_email_sent:
return # Already done, safe to return
send_email(user.email, "Welcome!")
# Mark as sent atomically
User.objects.filter(id=user_id, welcome_email_sent=False) \
.update(welcome_email_sent=True)
# If update affects 0 rows, another process already sent it
@app.task
def charge_customer(order_id, amount):
order = get_order(order_id)
if order.status == 'paid':
return # Already processed
# Use idempotency key with Stripe
idempotency_key = f"order-{order_id}-charge"
stripe.Charge.create(
amount=amount,
customer=order.customer_id,
idempotency_key=idempotency_key # Stripe prevents duplicates
)
order.status = 'paid'
order.save()
@app.task
def process_inventory(item_id, quantity, operation_id):
# Check if this specific operation was already processed
if InventoryOperation.objects.filter(id=operation_id).exists():
return # Already done
with transaction.atomic():
item = Item.objects.select_for_update().get(id=item_id)
item.stock -= quantity
item.save()
# Record that we did this operation
InventoryOperation.objects.create(id=operation_id, item_id=item_id)Idempotency patterns:
# 1. Unique operation ID
def process(operation_id, data):
if Operation.exists(operation_id):
return Operation.get(operation_id).result # Return cached result
result = do_work(data)
Operation.create(id=operation_id, result=result)
return result
# 2. State machine (can only transition once)
order.transition_to('shipped') # If already shipped, no-op
# 3. External idempotency keys (Stripe, payment providers)
stripe.Charge.create(idempotency_key=unique_key)
# 4. Conditional updates
UPDATE items SET stock = stock - 10
WHERE id = 123 AND stock >= 10 # Only if we have stockBenefits:
- Safe automatic retries
- Queue workers can use acks_late
- Duplicate messages don't cause problems
- Simpler error handling
Reference: The Twelve-Factor App - Disposability
Treat Logs as Event Streams Not Files
A twelve-factor app produces logs as a stream of time-ordered events, writing unbuffered to stdout. The app never concerns itself with routing or storage of its output stream - that's the responsibility of the execution environment.
Incorrect (app manages log files):
import logging
from logging.handlers import RotatingFileHandler
# App manages its own log files
handler = RotatingFileHandler(
'/var/log/myapp/app.log',
maxBytes=10000000,
backupCount=5
)
logger = logging.getLogger()
logger.addHandler(handler)
# Problems:
# - Container filesystem is ephemeral - logs lost on restart
# - Need to configure log rotation
# - Need to ship logs to aggregator separately
# - Different config per environment
# - Permission issues with /var/logCorrect (write to stdout):
import logging
import sys
# Configure logging to stdout
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s %(message)s'
)
logger = logging.getLogger(__name__)
def process_order(order_id):
logger.info(f"Processing order {order_id}")
# ... do work ...
logger.info(f"Order {order_id} completed")
# stdout captured by:
# - Docker: docker logs container_name
# - Kubernetes: kubectl logs pod_name
# - Heroku: heroku logs --tail
# - systemd: journalctl -u myappJSON structured logging:
import logging
import json
import sys
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
'timestamp': self.formatTime(record),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'extra': getattr(record, 'extra', {}),
})
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
logging.root.handlers = [handler]
logger = logging.getLogger(__name__)
logger.info("Order processed", extra={'extra': {'order_id': 123, 'amount': 99.99}})
# Output: {"timestamp": "2024-01-15 14:30:00", "level": "INFO", "logger": "__main__", "message": "Order processed", "extra": {"order_id": 123, "amount": 99.99}}Environment captures and routes:
# Kubernetes - logs go to stdout, platform routes
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
# App writes to stdout
# Kubernetes captures and stores
# Can ship to: Elasticsearch, Datadog, CloudWatch, etc.
# Docker Compose - logs to stdout
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Or ship to external service
# driver: fluentd
# options:
# fluentd-address: "localhost:24224"Benefits:
- App code is simple - just write to stdout
- Same code works in any environment
- Platform handles aggregation, rotation, shipping
- Real-time streaming and historical analysis
Reference: The Twelve-Factor App - Logs
Never Route or Store Logs from Within the Application
A twelve-factor app does not attempt to write to or manage logfiles. It does not configure log shipping, rotation, or aggregation. The execution environment handles capturing stdout, collating streams, and routing to destinations.
Incorrect (app routes logs):
import logging
import requests
from logging.handlers import HTTPHandler
# App sends logs to external service
datadog_handler = HTTPHandler(
host='http-intake.logs.datadoghq.com',
url='/v1/input/YOUR_API_KEY',
method='POST'
)
logger.addHandler(datadog_handler)
# Also write to file
file_handler = logging.FileHandler('/var/log/app.log')
logger.addHandler(file_handler)
# Also send to Sentry
sentry_sdk.init(dsn="https://xxx@sentry.io/xxx")
# Problems:
# - App knows about infrastructure (Datadog, Sentry URLs)
# - Credentials in app config
# - Network failures affect app
# - Different handlers per environment
# - Complex logging setupCorrect (app writes to stdout, platform routes):
import logging
import sys
# Simple: just stdout
logging.basicConfig(
stream=sys.stdout,
level=os.environ.get('LOG_LEVEL', 'INFO'),
format='%(asctime)s %(levelname)s %(message)s'
)
logger = logging.getLogger(__name__)
logger.info("Application started")
# App doesn't know or care where logs go
# Could be: file, Datadog, Elasticsearch, CloudWatch
# Decided by platform, not appPlatform handles routing:
# Kubernetes with Fluentd sidecar
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
# App writes to stdout
- name: fluentd
image: fluent/fluentd
volumeMounts:
- name: varlog
mountPath: /var/log
# Fluentd routes to Elasticsearch, S3, etc.# Docker with logging driver
services:
app:
logging:
driver: fluentd
options:
fluentd-address: "fluentd:24224"
tag: "app.{{.Name}}"
# App just writes to stdout
# Docker routes to Fluentd
# Fluentd routes to Elasticsearch# Heroku - logs automatically routed
heroku logs --tail
# Drains: heroku drains:add https://logs.papertrailapp.com/...
# AWS ECS - logs to CloudWatch automatically
# No app config neededBenefits:
- App code is simple
- Same app works with any log infrastructure
- Change log destination without changing app
- No credentials or infrastructure URLs in app
Reference: The Twelve-Factor App - Logs
Use Structured Logging for Machine-Readable Event Streams
While plain text logs are human-readable, structured logs (JSON) enable field-based querying, filtering, and analysis in log aggregation systems. Each log event becomes a queryable document with typed fields.
Incorrect (unstructured text logs):
logger.info(f"User {user_id} placed order {order_id} for ${amount}")
# Output: 2024-01-15 14:30:00 INFO User 123 placed order 456 for $99.99
# Problems:
# - Need regex to extract user_id, order_id, amount
# - Queries like "orders over $100" are difficult
# - Inconsistent formats across messages
# - Hard to correlate eventsCorrect (structured JSON logs):
import logging
import json
import sys
from datetime import datetime
class JSONFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat() + 'Z',
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
}
# Add extra fields
if hasattr(record, 'extra'):
log_data.update(record.extra)
return json.dumps(log_data)
# Configure root logger
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JSONFormatter())
logging.root.handlers = [handler]
logging.root.setLevel(logging.INFO)
logger = logging.getLogger(__name__)
# Log with structured data
logger.info("Order placed", extra={
'event': 'order.placed',
'user_id': 123,
'order_id': 456,
'amount': 99.99,
'currency': 'USD',
})
# Output: {"timestamp": "2024-01-15T14:30:00.000Z", "level": "INFO", "logger": "__main__", "message": "Order placed", "event": "order.placed", "user_id": 123, "order_id": 456, "amount": 99.99, "currency": "USD"}Using structlog for better ergonomics:
import structlog
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.BoundLogger,
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger()
# Clean syntax for structured logging
logger.info("order.placed", user_id=123, order_id=456, amount=99.99)
# Output: {"event": "order.placed", "user_id": 123, "order_id": 456, "amount": 99.99, "timestamp": "2024-01-15T14:30:00.000000Z"}
# Bind context that persists across calls
log = logger.bind(request_id="abc-123", user_id=123)
log.info("processing started")
log.info("step completed", step="validation")
log.info("processing finished")
# All three have request_id and user_idQuery structured logs:
# Elasticsearch/Kibana queries
event:order.placed AND amount:>100
# Datadog queries
@event:order.placed @amount:>100
# CloudWatch Insights
fields @timestamp, @message
| filter event = 'order.placed' and amount > 100
| stats count(*) by user_idBenefits:
- Query specific fields without regex
- Aggregate and analyze (orders per user, avg amount)
- Create alerts on specific conditions
- Correlate events by request_id or user_id
Reference: The Twelve-Factor App - Logs
Write Logs Unbuffered to Stdout for Real-Time Streaming
Each running process writes its event stream unbuffered to stdout. This ensures logs are visible immediately for real-time debugging and aren't lost if the process crashes before flushing buffers.
Incorrect (buffered output):
import sys
# Python buffers stdout by default when not connected to terminal
# Logs may not appear until buffer is full or process exits
print("Starting processing...")
# ... process runs for 10 minutes ...
print("Processing complete")
# If process crashes, both messages might be lost
# File-based logging with buffering
import logging
handler = logging.FileHandler('/var/log/app.log')
handler.setLevel(logging.INFO)
# Default mode is buffered - logs written in chunksCorrect (unbuffered stdout):
import sys
import logging
# Force unbuffered stdout
# Option 1: Environment variable
# PYTHONUNBUFFERED=1
# Option 2: In code
sys.stdout.reconfigure(line_buffering=True)
# Or for full unbuffering:
sys.stdout = sys.stderr = open(sys.stdout.fileno(), 'w', buffering=1)
# Configure logging with stream handler
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s'
)
logger = logging.getLogger(__name__)
# Each log appears immediately
logger.info("Starting processing...") # Visible immediately
# ... process runs ...
logger.info("Processing complete") # Visible immediatelyDockerfile configuration:
FROM python:3.11
# Ensure unbuffered output
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
# All print() and logging output is immediately visible
# docker logs -f container_name shows real-time outputNode.js unbuffered:
// Node.js stdout is unbuffered by default
// But console.log adds newlines, which helps
// For explicit control:
process.stdout.write('Processing...\n');
// Ensure flush on exit
process.on('exit', () => {
process.stdout.write(''); // Flush
});Why unbuffered matters:
# Real-time debugging
kubectl logs -f deployment/web
# See each log line as it happens
# Crash investigation
# Process crashed - last logs are visible because unbuffered
# With buffering, last 4KB might be lost
# Streaming to log aggregator
# Fluentd/Logstash receives events immediately
# Alerting can trigger in real-timeBenefits:
docker logs -fandkubectl logs -fshow real-time output- No log loss on crash
- Real-time alerting possible
- Debugging live issues is straightforward
Reference: The Twelve-Factor App - Logs
Deploy Frequently to Minimize the Time Gap
The twelve-factor developer makes the time gap small: code should be deployed within hours or minutes of being written, not weeks or months. Frequent small deploys are less risky than infrequent large deploys.
Incorrect (infrequent deploys):
Development Timeline:
Week 1: Feature A developed
Week 2: Feature B developed
Week 3: Feature C developed
Week 4: Code review for all features
Week 5: QA testing
Week 6: Staging deployment
Week 7: Production deployment
Problems:
- 6 weeks of changes in one deploy
- If something breaks, hard to identify which feature
- Developers don't remember the context
- Rollback means losing 3 features
- Large merge conflictsCorrect (frequent deploys):
Continuous Deployment Timeline:
Monday 9am: Feature A merged → deployed to staging → promoted to production
Monday 2pm: Bug fix merged → deployed → production
Tuesday 11am: Feature B merged → deployed → production
Wednesday 9am: Feature C merged → deployed → production
Benefits:
- Each deploy is small and understandable
- If something breaks, obvious which commit caused it
- Context is fresh in developer's mind
- Rollback loses only one small change
- No merge conflictsCI/CD pipeline for frequent deploys:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
deploy-staging:
needs: test
runs-on: ubuntu-latest
steps:
- run: deploy-to-staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- run: deploy-to-productionFeature flags for incomplete features:
# Deploy incomplete feature behind flag
from feature_flags import is_enabled
@app.route('/new-dashboard')
def new_dashboard():
if is_enabled('new_dashboard', current_user):
return render_template('new_dashboard.html')
return redirect('/dashboard')
# Can deploy partial implementation daily
# Enable for small percentage to test
# Roll out gradually or instant when readySmall, focused commits:
# Bad: large commit with many changes
git commit -m "Add user dashboard, fix login bug, update deps"
# Good: small, focused commits
git commit -m "Add user dashboard route"
git commit -m "Add dashboard template"
git commit -m "Fix login redirect bug"
git commit -m "Update security dependencies"
# Each can be deployed (and rolled back) independentlyBenefits:
- Lower risk per deployment
- Faster feedback from production
- Easier debugging when issues arise
- Continuous improvement mindset
Reference: The Twelve-Factor App - Dev/prod parity
Involve Developers in Deployment to Minimize Personnel Gap
In a twelve-factor workflow, developers who write code are closely involved in deploying it and watching its behavior in production. The traditional wall between "dev" and "ops" creates a personnel gap that slows feedback and obscures problems.
Incorrect (separated dev and ops):
Traditional Workflow:
1. Developer writes code
2. Developer marks ticket "ready for deploy"
3. Wait for deployment window (next Tuesday)
4. Ops team deploys during maintenance window
5. Ops notices errors in monitoring
6. Ops creates ticket for dev team
7. Dev team investigates (no production access)
8. Dev asks ops to run queries
9. Back and forth for days
10. Fix deployed next weekCorrect (developers deploy and observe):
Twelve-Factor Workflow:
1. Developer writes code
2. Developer pushes to main
3. CI/CD automatically deploys to staging
4. Developer verifies staging
5. Developer promotes to production (one-click)
6. Developer watches metrics and logs
7. Developer sees error spike immediately
8. Developer investigates with production access
9. Developer deploys fix within hoursEnable developer deployment:
# Deployment should be simple enough for any developer
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
- run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# Anyone can trigger production deploy via merge to main
# No special ops knowledge requiredDeveloper access to production:
# Developers can view logs
kubectl logs -f deployment/web
# Developers can access REPL
kubectl exec -it deployment/web -- python manage.py shell
# Developers can run migrations
kubectl exec -it deployment/web -- python manage.py migrate
# Developers can view metrics
# Link to Grafana/Datadog dashboard in README
# Developers can roll back
kubectl rollout undo deployment/webSelf-service deployment:
# Developer can deploy without ops involvement
git push origin main # Triggers CI/CD
# Or manual promotion
./scripts/promote-to-production.sh
# Emergency rollback
./scripts/rollback-production.sh
# All actions logged and auditable
# No gatekeeping, just guardrailsBenefits:
- Faster feedback loops
- Developers understand production behavior
- Issues resolved quickly by people with context
- Shared responsibility improves quality
Reference: The Twelve-Factor App - Dev/prod parity
Minimize Gaps Between Development and Production Environments
A twelve-factor app minimizes the gap between development and production in three dimensions: time (deploy frequently), personnel (developers are involved in deploys), and tools (use the same backing services everywhere).
The three gaps:
| Gap | Traditional App | Twelve-Factor App |
|---|---|---|
| Time | Weeks between deploys | Hours or minutes |
| Personnel | Devs write, ops deploy | Same person does both |
| Tools | SQLite dev, PostgreSQL prod | PostgreSQL everywhere |
Incorrect (large gaps):
# Different databases per environment
if os.environ.get('ENV') == 'development':
# SQLite for development - "easier"
DATABASE_URL = 'sqlite:///dev.db'
else:
# PostgreSQL for production
DATABASE_URL = os.environ['DATABASE_URL']
# Code that works in SQLite but fails in PostgreSQL:
# - Different SQL syntax
# - Different type handling
# - Different transaction behavior
# - Missing features (JSON operators, array types)
# Deploy process:
# 1. Developer finishes feature
# 2. Waits for code review (days)
# 3. Waits for QA (days)
# 4. Ops team deploys (weekend)
# 5. Bug found in production (SQLite vs Postgres difference)Correct (minimal gaps):
# Same database type everywhere
DATABASE_URL = os.environ['DATABASE_URL']
# Development: postgresql://localhost:5432/myapp_dev
# Production: postgresql://prod-db:5432/myapp
# Same backing services
REDIS_URL = os.environ['REDIS_URL']
# Development: redis://localhost:6379
# Production: redis://prod-redis:6379# docker-compose.yml for development
# Mirrors production stack
services:
db:
image: postgres:15 # Same version as production
redis:
image: redis:7 # Same version as production
app:
environment:
- DATABASE_URL=postgresql://db:5432/myapp
- REDIS_URL=redis://redis:6379# CI/CD - deploy frequently
# Every merge to main deploys to staging
# One-click promotion to production
deploy:
stage: deploy
script:
- deploy-to-staging
only:
- main
promote:
stage: promote
script:
- promote-staging-to-production
when: manual # But still same dayBenefits:
- Bugs caught in development, not production
- Developers understand deployment
- Continuous deployment becomes possible
- Faster feedback loops
Reference: The Twelve-Factor App - Dev/prod parity
Use the Same Type and Version of Backing Services in All Environments
The twelve-factor developer resists the urge to use different backing services between development and production. Using SQLite locally and PostgreSQL in production, or mocking Redis in tests, creates subtle bugs that only appear in production.
Incorrect (different backing services):
# settings.py
if os.environ.get('ENV') == 'development':
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Easy to set up
'NAME': 'db.sqlite3',
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
# ...
}
}
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.environ['REDIS_URL'],
}
}
# Bugs from:
# - SQLite vs PostgreSQL query differences
# - In-memory cache vs Redis behavior differences
# - Different transaction semanticsCorrect (same backing services):
# settings.py - same services everywhere
DATABASES = {
'default': dj_database_url.config(default='postgresql://localhost/myapp')
}
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': os.environ.get('REDIS_URL', 'redis://localhost:6379/0'),
}
}# docker-compose.yml - production stack locally
version: '3.8'
services:
db:
image: postgres:15.4 # Same version as production
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: localdev
redis:
image: redis:7.2 # Same version as production
elasticsearch:
image: elasticsearch:8.10.2 # Same version as production
app:
build: .
environment:
- DATABASE_URL=postgresql://myapp:localdev@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
- ELASTICSEARCH_URL=http://elasticsearch:9200
depends_on:
- db
- redis
- elasticsearch
volumes:
pgdata:Modern tools make this easy:
# Docker makes running real services trivial
docker run -d -p 5432:5432 postgres:15
docker run -d -p 6379:6379 redis:7
# Package managers have database packages
brew install postgresql@15
brew install redis
# Cloud emulators for cloud services
# LocalStack for AWS
docker run -d -p 4566:4566 localstack/localstack
# Firebase emulator for Google services
firebase emulators:startBenefits:
- Same query behavior in development and production
- Same caching semantics everywhere
- No "it works in SQLite" production bugs
- Tests reflect real production behavior
Reference: The Twelve-Factor App - Dev/prod parity
Use Port Binding to Export Any Protocol Not Just HTTP
The port-binding pattern applies to any protocol, not just HTTP. Applications can export services via SMTP, Redis protocol, gRPC, WebSocket, or any custom protocol. This enables one twelve-factor app to serve as a backing service for another.
Incorrect (protocol-specific deployment requirements):
# gRPC service requiring special server injection
# Assumes specific infrastructure for gRPC routing
class UserService:
def GetUser(self, request):
return User(id=request.id)
# Deployed via gRPC-specific server manager
# Can't use same deployment model as HTTP services
# Requires different infrastructure per protocolCorrect (all protocols via port binding):
# gRPC service self-contained with port binding
import grpc
from concurrent import futures
import os
class UserService(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
return user_pb2.User(id=request.id, name="Alice")
def serve():
port = os.environ.get('PORT', '50051')
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port(f'[::]:{port}')
server.start()
server.wait_for_termination()
# Same deployment model as HTTP - container binds to PORT**HTTP service (common case):
# HTTP API service
from flask import Flask
app = Flask(__name__)
@app.route('/api/users/<id>')
def get_user(id):
return {"id": id, "name": "Alice"}
# Bind to port
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))gRPC service:
# gRPC service on port
import grpc
from concurrent import futures
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
return user_pb2.User(id=request.id, name="Alice")
def serve():
port = os.environ.get('PORT', '50051')
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port(f'[::]:{port}')
server.start()
server.wait_for_termination()WebSocket service:
import asyncio
import websockets
import os
async def handler(websocket, path):
async for message in websocket:
await websocket.send(f"Echo: {message}")
async def main():
port = int(os.environ.get('PORT', 8765))
async with websockets.serve(handler, "0.0.0.0", port):
await asyncio.Future() # Run forever
asyncio.run(main())One app as backing service for another:
# docker-compose.yml
services:
# User service exposes gRPC
user-service:
build: ./user-service
environment:
- PORT=50051
- DATABASE_URL=postgresql://db:5432/users
# API gateway consumes user service
api-gateway:
build: ./api-gateway
environment:
- PORT=8080
- USER_SERVICE_URL=grpc://user-service:50051
depends_on:
- user-service# API gateway consuming user service
USER_SERVICE_URL = os.environ['USER_SERVICE_URL'] # grpc://user-service:50051
# User service is an attached resource, just like a database
# Can swap implementations without changing API gateway codeBenefits:
- Microservices can be backing services for each other
- Service mesh handles routing regardless of protocol
- Same deployment model for HTTP, gRPC, WebSocket, etc.
- Easy to swap service implementations
Reference: The Twelve-Factor App - Port Binding
Export Services via Port Binding Using PORT Environment Variable
A twelve-factor app binds to a port specified by the environment (typically the PORT variable) to export its service. This allows the execution environment (Kubernetes, Heroku, Docker) to assign ports dynamically and route traffic appropriately.
Incorrect (hardcoded port):
# Port hardcoded - can't run multiple instances on same host
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
# What if port 8080 is taken?
# How does Heroku/Railway tell the app which port to use?// Port hardcoded in code
const PORT = 3000; // Always 3000, no flexibility
server.listen(PORT);Correct (port from environment):
import os
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
app.run(host='0.0.0.0', port=port)
# Platform sets PORT, app obeys
# Default 8080 for local developmentconst PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.ListenAndServe(":"+port, nil)How platforms use PORT:
# Heroku sets PORT automatically
# Your Procfile just specifies the command
web: gunicorn app:app
# Kubernetes - port exposed in container spec
spec:
containers:
- name: app
env:
- name: PORT
value: "8080"
ports:
- containerPort: 8080
# Docker - map host port to container port
docker run -e PORT=8080 -p 80:8080 myapp
# External port 80 → Container port 8080
# Docker Compose - multiple instances
services:
app:
environment:
- PORT=8080
deploy:
replicas: 3
# Load balancer routes to any replica's 8080Benefits:
- Platform can run multiple app instances with different ports
- Zero-downtime deploys: new instances on new ports while draining old
- Service mesh integration works automatically
- Local development can specify any available port
Reference: The Twelve-Factor App - Port Binding
Make the Application Completely Self-Contained with Embedded Server
A twelve-factor app is completely self-contained and does not rely on runtime injection of a webserver. The app exports HTTP (or other protocols) as a service by binding to a port, using a webserver library that is part of the application's dependencies.
Incorrect (requires external webserver):
# Apache config - app requires Apache to run
<VirtualHost *:80>
ServerName myapp.com
WSGIScriptAlias / /var/www/myapp/wsgi.py
# App cannot run without Apache
# Different servers need different configs
# Deployment complexity increases
</VirtualHost><!-- PHP requires Apache/nginx mod_php or php-fpm -->
<!-- index.php cannot listen on a port by itself -->
<?php
// No way to "run" this standalone
echo "Hello World";
?>Correct (self-contained with embedded server):
# Python with Gunicorn - webserver is a dependency
# requirements.txt: gunicorn==21.2.0
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello World"
# Run with: gunicorn app:app --bind 0.0.0.0:$PORT
# No Apache/nginx required - app binds directly to port// Node.js - http server is built into the language
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
// No external webserver needed// Go - standard library includes http server
package main
import (
"net/http"
"os"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.ListenAndServe(":"+port, nil)
}
// Single binary serves HTTP directlyBenefits:
docker run -p 8080:8080 myappjust works- Same deployment model across all languages
- Local development matches production
- Platform assigns port, app binds to it
Reference: The Twelve-Factor App - Port Binding
Perform Asset Compilation and Bundling at Build Time Not Runtime
Asset compilation (JavaScript bundling, Sass compilation, image optimization) should happen during the build stage, not at runtime. The running process should serve pre-compiled assets, not compile on demand.
Incorrect (runtime compilation):
# Django with runtime asset compilation
from django.conf import settings
# In development, this compiles on each request
# In production, this might work initially...
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
# But in 12-factor app:
# - Container restarts lose compiled assets
# - Each instance recompiles (wasted CPU, slow startup)
# - First request after deploy is slow
# - Compilation errors happen in production// Node.js with runtime bundling
const webpack = require('webpack');
const middleware = require('webpack-dev-middleware');
// Running webpack in production!
app.use(middleware(webpack(config)));
// Compiles on startup - slow
// Compiles on change - unnecessary in production
// Memory overhead of compiler in productionCorrect (build-time compilation):
# Multi-stage build with asset compilation
FROM node:20 AS assets
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # Webpack/Vite/esbuild runs HERE
FROM python:3.11-slim
WORKDIR /app
COPY --from=assets /app/dist /app/static # Pre-built assets
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "app:application"]
# No asset compilation at runtime
# Static files are ready to serve immediately# Django serving pre-built static files
STATIC_URL = '/static/'
STATIC_ROOT = '/app/static'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# python manage.py collectstatic runs at BUILD time
# At runtime, files are already in STATIC_ROOT# CI pipeline
build:
script:
- npm ci
- npm run build # Compile assets
- python manage.py collectstatic --noinput
- docker build -t myapp:$SHA .Benefits:
- Fast startup: no compilation at process start
- Consistent: all instances serve identical assets
- Early failure: compilation errors fail the build, not production
- Reduced runtime resources: no compiler in production memory
Reference: The Twelve-Factor App - Processes
Never Assume Local Filesystem Persists Between Requests
The filesystem of a twelve-factor app process is ephemeral. Any file written will be lost when the process restarts, the container is replaced, or the instance is terminated. Use backing services for all persistent storage.
Incorrect (persisting to local filesystem):
import os
UPLOAD_DIR = '/var/uploads'
def save_upload(file):
# Writing to local filesystem
path = os.path.join(UPLOAD_DIR, file.filename)
file.save(path)
return path
# File exists only on THIS container/instance
# Lost on restart, invisible to other instances
def get_upload(filename):
path = os.path.join(UPLOAD_DIR, filename)
return open(path, 'rb')
# If request routes to different instance: FileNotFoundError# Volume mount doesn't help in orchestrated environments
FROM python:3.11
VOLUME /var/uploads
# This volume is local to the container
# Kubernetes will create a new volume each pod restartCorrect (external persistent storage):
import boto3
import os
s3 = boto3.client('s3')
BUCKET = os.environ['UPLOAD_BUCKET']
def save_upload(file):
# Store in S3 (or any object storage)
key = f"uploads/{file.filename}"
s3.upload_fileobj(file, BUCKET, key)
return f"s3://{BUCKET}/{key}"
# Accessible from any process, survives restarts
def get_upload(filename):
key = f"uploads/{filename}"
response = s3.get_object(Bucket=BUCKET, Key=key)
return response['Body']
# Works from any instanceAcceptable temporary filesystem use:
import tempfile
import os
def process_large_file(file):
# Temporary file for processing within single request
with tempfile.NamedTemporaryFile(delete=False) as tmp:
file.save(tmp.name)
try:
# Process the file
result = expensive_operation(tmp.name)
# Store result in backing service
s3.upload_file(result, BUCKET, 'results/output.csv')
finally:
# Clean up temp file
os.unlink(tmp.name)
# Temp file used only during this request
# Result persisted externallyBenefits:
- App can run in ephemeral containers (Kubernetes, ECS, etc.)
- Restarts don't lose data
- Horizontal scaling works - all instances see same data
- Disaster recovery is straightforward (backing service handles it)
Reference: The Twelve-Factor App - Processes
Never Use Sticky Sessions - Store Session Data in Backing Services
Sticky sessions (affinity) route users to the same server instance, creating state in the process. This is a violation of twelve-factor. Session state should be stored in a backing service with time-expiration, like Redis or Memcached, so any process can handle any request.
Incorrect (sticky sessions):
# Flask with default session (stored in signed cookie or process memory)
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'secret'
@app.route('/login')
def login():
session['user_id'] = get_user_id()
session['cart'] = [] # Shopping cart in session
# Session data is in process memory or oversized cookie
# Load balancer must route this user to same server# nginx sticky session config (the problem)
upstream backend {
ip_hash; # Routes based on client IP - creates affinity
server backend1:8000;
server backend2:8000;
}
# If backend1 goes down, all its users lose their sessionsCorrect (external session store):
from flask import Flask
from flask_session import Session
import redis
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url(os.environ['REDIS_URL'])
app.config['SESSION_PERMANENT'] = False
Session(app)
@app.route('/login')
def login():
session['user_id'] = get_user_id()
session['cart'] = []
# Session stored in Redis with automatic expiration
# Any backend server can read/write this session# nginx without sticky sessions
upstream backend {
# Round-robin (default) - no affinity needed
server backend1:8000;
server backend2:8000;
server backend3:8000;
}
# Any server can handle any request
# Server failure doesn't lose sessionsSession storage options:
# Redis - fast, supports expiration
SESSION_REDIS = redis.from_url(os.environ['REDIS_URL'])
# PostgreSQL - if you need transactional guarantees
SESSION_SQLALCHEMY = SQLAlchemy(app)
# Memcached - simple, volatile (acceptable for sessions)
SESSION_MEMCACHED = pylibmc.Client([os.environ['MEMCACHED_URL']])Benefits:
- Load balancer can route any request anywhere
- Servers can be added/removed without session impact
- Server crash doesn't log out users
- Scaling is linear - just add servers
Reference: The Twelve-Factor App - Processes
Related skills
FAQ
What does 12-factor-app do?
12-factor-app: A skill for development. This provides functionality for development workflows.
When should I use 12-factor-app?
When you need to use 12-factor-app for development tasks, or when 12-factor-app: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
12-factor-app.