Intro: The Hidden Cost of a Dirty .env
At one point, our .env
file had it all: API keys, credentials, feature flags, hardcoded secrets, region-specific toggles… you name it. As our app scaled past thousands of daily users and into the million+ territory, it became clear: this was no longer just a file—it was a liability.
We needed to clean up, or face the consequences:
- Security breaches from exposed secrets
- Onboarding nightmares
- Broken staging/production parity
- Inconsistent deploys
- “It works on my machine” bugs
Enter the 12-Factor App methodology.
This isn’t just for Heroku-era apps or monolith-to-microservice transitions. These 12 principles are a battle-tested blueprint for building cloud-native, scalable, and maintainable software. And yes, they’ll help keep your .env
files clean—even at enterprise scale.
Let’s Break It Down: The 12 Factors + Real-World Lessons
1. Codebase: One Codebase, Many Deploys
“A single codebase tracked in version control, many deploys.”
- One app = one repo. No branching or duplicating repos per environment.
- Use feature flags, env-specific settings—not divergent codebases.
Impact: Keeps configuration DRY and scoped only to deploys. .env
doesn’t need to support multiple environments in one file.
2. Dependencies: Explicitly Declare and Isolate
“Never rely on implicit system-wide packages.”
- Use package managers (
npm
,pip
,go mod
) to declare every dependency. - Use virtual environments or containers to avoid global package conflicts.
Impact: No need to bake dependency paths into .env
. Keeps builds reproducible and clean.
3. Config: Store Config in the Environment
“Store config in env vars, not in code.”
This is the core principle behind our clean .env
. Instead of:
DATABASE_URL=postgres://user:pass@host:5432/db
API_KEY=sk_test_abcd1234
… we do this:
- Use secret managers (AWS Secrets Manager, HashiCorp Vault, Doppler).
- Inject values during deployment (Kubernetes secrets, GitHub Actions).
- Keep
.env
for local development only (and never include secrets).
NODE_ENV=development
LOG_LEVEL=debug
Impact: Secrets are no longer developer-accessible or in source control. The .env
file becomes a safe, environment-specific tool—not a risk.
4. Backing Services: Treat Services as Attached Resources
“Whether it’s local or third-party, treat services like interchangeable components.”
- Connect to DBs, caches, queues using env vars.
- Avoid hardcoding service hostnames or credentials.
Example:
REDIS_URL=redis://cache.internal:6379
Impact: No service-specific logic pollutes the .env
. Switching a service is a config change, not a code change.
5. Build, Release, Run: Separate Build from Deploy
“Strictly separate the three stages: build, release, run.”
- Build once (e.g., Docker image), promote the same build across environments.
- Inject config/secrets at runtime using environment-specific deploy settings.
Impact: You don’t bake secrets into builds. The .env
stays minimal, tied only to runtime needs.
6. Processes: Stateless and Share-Nothing
“Run the app as stateless processes.”
- All state (user sessions, jobs, etc.) stored in external backing services.
- Each process is replaceable and restartable.
Impact: No need to track session or user context via environment variables. Clean .env
, no state leakage.
7. Port Binding: Self-Contained Services
“The app should be entirely self-contained and expose a port.”
- Web servers, APIs, workers all bind to a port internally and can be reverse-proxied or orchestrated.
- No external dependencies on runtime behavior.
Impact: No need for .env
to track upstream server dependencies or system-level configurations. Simplicity wins.
8. Concurrency: Scale with Processes
“Use process types for horizontal scaling.”
- Use multiple process types:
web
,worker
,cron
. - Scale each independently via orchestration (K8s, ECS, etc.).
Impact: Each process reads minimal config from .env
, keeping scope small and decoupled.
9. Disposability: Fast Startup and Graceful Shutdown
“Apps should start and stop quickly for fast deploys and scaling.”
- Startup time < 1s
- Handle SIGTERM/SIGINT for clean exits
Impact: No need to manage persistent config or app-specific state in .env
. Secrets are injected once per start, not updated on the fly.
10. Dev/Prod Parity: Keep All Environments Aligned
“Minimize differences between dev, staging, and prod.”
- Same services, same configs, same env vars across all environments.
- Use
.env.example
to document expected vars.
DB_URL
REDIS_URL
STRIPE_KEY
Impact: The .env
file is self-documenting. Developers know exactly what to expect across environments. No mystery variables.
11. Logs: Treat Logs as Event Streams
“Never write logs to files—stream to stdout.”
- Logs go to console → collected by a logging system (e.g., Fluentd, Datadog).
- No need for log paths, file permissions, or file rotation configs in
.env
.
Impact: Removes file system clutter from .env
. Logging becomes infrastructure-managed.
12. Admin Processes: One-Off Tasks
“Run admin tasks as one-time commands in the same environment.”
- Run DB migrations, data backfills, cleanups as ad-hoc CLI tasks.
- Use the same env vars and config as the main app.
Impact: You don’t need a second .env
for admin tools. Same config, same behavior.
12-factor app example
Example: Laravel E-Commerce API
Imagine you’re building a Laravel-based API for an e-commerce app (e.g., AcmeShop
). Here’s how the 12-Factor principles would apply.
1. Codebase
One codebase tracked in Git, many deploys (staging, production, etc.)
- One Git repo:
github.com/acme/laravel-shop
- Separate deploys for
production
,staging
,local
git clone git@github.com:acme/laravel-shop.git
2. Dependencies
Declare dependencies explicitly with Composer.
Use composer.json
:
"require": {
"laravel/framework": "^10.0",
"guzzlehttp/guzzle": "^7.0"
}
No global PHP packages. Use .php-version
and composer.lock
for version consistency.
3. Config
Store config in environment variables.
Laravel reads from .env
, but never commit .env
to Git.
.env.example
:
APP_ENV=production
APP_KEY=base64:...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=shop
DB_USERNAME=root
DB_PASSWORD=secret
For production:
- Use AWS Parameter Store, Secrets Manager, or Laravel Envoy to inject secrets.
- Avoid secret sprawl—store only non-sensitive dev defaults locally.
4. Backing Services
Treat services like the database, Redis, S3, and Mailgun as attachable resources.
Configure via env:
REDIS_URL=tcp://127.0.0.1:6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
You can swap Redis for Memcached or Mailgun for SES without touching code.
5. Build, Release, Run
Keep build, release, and run phases separate.
Build:
bashCopyEditcomposer install --no-dev --optimize-autoloader
php artisan config:cache
Release:
Pull in env vars and migrations:
php artisan migrate --force
Run:
php artisan serve
In production: run with Nginx or PHP-FPM, managed by a supervisor or container.
6. Processes
Laravel processes (like web and queues) are stateless.
- Session storage is offloaded to Redis or DB.
- Queues (like
email
,jobs
) use Laravel Horizon orphp artisan queue:work
.
No local state. You can scale horizontally.
7. Port Binding
Laravel doesn’t bind ports directly, but when using
artisan serve
, it does:
php artisan serve --port=8000
In production, Laravel runs behind PHP-FPM or Nginx. Containers like Docker expose ports as needed (EXPOSE 9000
for FPM).
8. Concurrency
Laravel supports concurrency with workers:
Use supervisord
or queue:work
processes:
php artisan queue:work redis --queue=high,default,low
Scale web
and worker
processes independently in Docker, Kubernetes, or Forge.
9. Disposability
Fast boot and graceful shutdown.
Ensure:
- Laravel starts in <1 second (optimize autoload, cache config).
- Handle signals (
SIGTERM
) in queue workers:
pcntl_signal(SIGTERM, function() {
Log::info('Graceful shutdown...');
exit;
});
10. Dev/Prod Parity
Keep environments as similar as possible.
Use the same .env
keys across environments. Differences are only in values.
Use:
- Laravel Sail for Docker-based local dev
- Laravel Forge or Envoyer for prod/staging
- Stubs like
.env.example
to document variables
11. Logs
Log to stdout or Laravel’s built-in logging driver.
In .env
:
LOG_CHANNEL=stdout
Configure logging.php
:
'channels' => [
'stdout' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => ['stream' => 'php://stdout'],
],
],
This is perfect for Docker or serverless environments where logs are captured by the platform.
12. Admin Processes
One-off admin tasks (e.g., migrations, seeding) run via CLI.
php artisan migrate
php artisan db:seed
php artisan queue:restart
You can run these as isolated container commands or jobs.
Summary
Factor | Laravel Implementation |
---|---|
Codebase | One Git repo, multiple envs |
Dependencies | Composer-managed |
Config | .env , injected securely |
Backing Services | Redis, DB, mail via env |
Build/Release/Run | Separate deploy steps |
Processes | Stateless Laravel workers |
Port Binding | artisan serve / FPM |
Concurrency | Workers with queue:work |
Disposability | Fast boot, signal handling |
Dev/Prod Parity | Sail, Forge, and same .env.example |
Logs | Log to stdout |
Admin Processes | Artisan CLI tasks |
Checklist to Clean Your .env
Here’s how you can start cleaning up:
- Identify and move secrets to a vault or cloud secret manager.
- Replace secret values with env var references in your app.
- Create
.env.example
as a documentation-only template. - Keep
.env
under.gitignore
and out of version control. - Remove unused or legacy vars quarterly.
- Audit all environment variables in CI/CD pipelines.
Conclusion: Clean Config Is Scalable Config
Clean .env
files aren’t just about cleanliness—they’re a sign of software maturity.
By following the 12-Factor App methodology, we’ve scaled to millions of daily users without:
- Security risks
- Deployment inconsistencies
- Hard-to-debug issues
- Painful onboarding
The .env
file has become what it was always meant to be: a clean, clear interface between code and runtime.
FAQ: 12-Factor App for Laravel Developers
1. What is the 12-Factor App and why should I use it with Laravel?
The 12-Factor App is a set of software engineering best practices for building scalable, maintainable, cloud-native applications. Applying it to Laravel helps you keep configuration clean, separate secrets from code, simplify deployments, and scale your app with confidence.
2. Should I stop using .env
in Laravel if I follow 12-Factor principles?
No—you should still use .env
, but wisely. Use .env
only for local development, and keep it minimal. In production, inject environment variables securely using secret managers or your CI/CD pipeline. Never commit .env
files or store secrets in version control.
3. How do I manage secrets in Laravel securely at scale?
Use tools like AWS Secrets Manager, HashiCorp Vault, or Laravel Envoyer to inject secrets at runtime. Laravel will read them via env()
without relying on .env
files in production. You can also set them via container environments (e.g., Docker, Kubernetes).
4. Can I apply 12-Factor principles without using Docker or Kubernetes?
Yes. Docker and Kubernetes help, but they’re not required. You can follow 12-Factor principles using standard Laravel deployment tools (like Forge, Envoyer, GitHub Actions) by keeping environments consistent, separating config, and using Laravel’s CLI and logging tools properly.
5. Will following the 12-Factor App make my Laravel app slower or more complex?
Not at all. In fact, it makes your app more modular, maintainable, and secure. There’s a learning curve at first—especially around secret handling and process separation—but long-term it reduces bugs, config drift, and deployment headaches.
Further Reading & References
For a comprehensive understanding of the principles that helped us keep our .env
clean and our Laravel app scalable, check out the official 12-Factor App website:
- The 12-Factor App: Introduction
- Factor III: Config – Storing config in the environment
- Factor VI: Processes – Stateless processes
- Factor XI: Logs – Treat logs as event streams
These resources provide detailed guidance on each factor and practical examples for modern app development.
Comments