| name | twelve-factor-app-modernization |
| description | Systematic process for analyzing and modernizing legacy applications to comply with the 12-Factor App methodology. Covers code analysis, configuration externalization, dependency management, and container/Kubernetes best practices.
|
| license | MIT |
| metadata | {"author":"Stakpak <team@stakpak.dev>","version":"1.0.22"} |
12-Factor App Modernization
Quick Start
Analyze Application
git clone <repo-url>
cd <repo-name>
Generate Compliance Report
Create a structured JSON report documenting compliance status for all 12 factors.
The 12 Factors
| Factor | Principle | Key Questions |
|---|
| I. Codebase | One codebase, many deploys | Is there a single repo? Are there environment-specific branches? |
| II. Dependencies | Explicitly declare and isolate | Are all dependencies declared with pinned versions? |
| III. Config | Store config in environment | Are connection strings, credentials, and settings externalized? |
| IV. Backing Services | Treat as attached resources | Can backing services be swapped without code changes? |
| V. Build, Release, Run | Strict separation | Are build artifacts immutable? Is config injected at runtime? |
| VI. Processes | Stateless processes | Is session state stored externally? |
| VII. Port Binding | Export via port binding | Are services self-contained with embedded servers? |
| VIII. Concurrency | Scale via process model | Can the app scale horizontally? |
| IX. Disposability | Fast startup, graceful shutdown | Does the app handle SIGTERM? Is there an init system (tini)? |
| X. Dev/Prod Parity | Keep environments similar | Are dev and prod using same backing services? |
| XI. Logs | Treat as event streams | Does the app write to stdout/stderr? |
| XII. Admin Processes | Run as one-off processes | Are migrations and admin tasks separate from the main app? |
Application Analysis
Step 1: Clone and Inventory
Map out the application architecture:
- Identify all services/microservices
- Document the tech stack for each service (language, framework, runtime)
- List all deployment configurations (Dockerfiles, docker-compose, Kubernetes manifests)
- Identify backing services (databases, caches, message queues)
Step 2: Analyze Against Each Factor
Systematically evaluate each service against all 12 factors. Create a structured report documenting compliance status.
Compliance Report Structure
{
"title": "12-Factor App Compliance Report",
"repository": "<repo-url>",
"analysis_date": "<date>",
"summary": {
"total_factors": 12,
"services_analyzed": ["<service-list>"],
"overall_compliance": "<status>"
},
"factors": {
"<factor_id>": {
"name": "<factor-name>",
"status": "<COMPLIANT|PARTIAL|NON-COMPLIANT>",
"findings": ["<observations>"],
"issues": [{"service": "", "file": "", "problem": "", "severity": "", "impact": ""}],
"fixes": [{"service": "", "file": "", "action": ""}]
}
},
"priority_fixes": [{"priority": 1, "category": "", "description": "", "affected_files": []}]
}
Configuration Externalization (Factor III - Highest Priority)
This is typically the most critical violation. Hardcoded credentials and connection strings are security risks.
Python (Flask/Django)
redis = Redis(host="redis", port=6379)
redis_host = os.getenv('REDIS_HOST', 'redis')
redis_port = int(os.getenv('REDIS_PORT', '6379'))
redis = Redis(host=redis_host, port=redis_port)
Node.js
const pool = new Pool({
connectionString: 'postgres://user:pass@db/mydb'
});
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgres://user:pass@db/mydb'
});
C#/.NET
var conn = new NpgsqlConnection("Server=db;Username=postgres;Password=secret;");
var host = Environment.GetEnvironmentVariable("DB_HOST") ?? "db";
var user = Environment.GetEnvironmentVariable("DB_USER") ?? "postgres";
var password = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "postgres";
var connString = $"Server={host};Username={user};Password={password};";
Dependency Management (Factor II)
Unpinned dependencies cause reproducibility issues and potential security vulnerabilities.
Python (requirements.txt)
# Before
Flask
Redis
gunicorn
# After
Flask==3.0.0
Redis==5.0.1
gunicorn==21.2.0
Node.js (package.json)
"dependencies": {
"express": "^4.18.2"
}
"dependencies": {
"express": "4.18.2"
}
Important: After changing package.json versions, regenerate package-lock.json:
rm package-lock.json && npm install --package-lock-only
Signal Handling (Factor IX)
Containers need proper init systems to handle signals correctly for graceful shutdown.
Add tini to Dockerfiles
# Install tini
RUN apt-get update && \
apt-get install -y --no-install-recommends tini && \
rm -rf /var/lib/apt/lists/*
# Use tini as entrypoint
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["your-app-command"]
Docker Compose Configuration
Replace hardcoded values with environment variable substitution:
services:
app:
environment:
DATABASE_URL: "postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db/mydb"
REDIS_HOST: redis
db:
environment:
POSTGRES_USER: "${POSTGRES_USER:-postgres}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-postgres}"
Kubernetes Secrets
Never store credentials in plain text in Kubernetes manifests.
Create Secret Manifest
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: <generate-strong-password>
Reference in Deployments
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: POSTGRES_PASSWORD
Validation
Docker Compose Validation
docker compose config --quiet && echo "✓ Valid"
Kubernetes Manifest Validation
kubectl apply --dry-run=client -f k8s-specifications/
Build and Test
docker compose build
docker compose up -d
docker compose ps
Common Anti-Patterns
- Hardcoded connection strings - Most common and most critical
- Unpinned dependencies - Causes "works on my machine" issues
- Missing init systems - Causes zombie processes and slow shutdowns
- Credentials in source control - Security vulnerability
- Environment-specific code branches - Violates codebase principle
Environment Variable Naming Conventions
Use consistent, descriptive names:
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME
REDIS_HOST, REDIS_PORT
DATABASE_URL (for connection string format)
<SERVICE>_URL for full connection strings
Backward Compatibility
Always provide sensible defaults when externalizing config:
host = os.getenv('REDIS_HOST', 'redis')
References