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 or php 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

FactorLaravel Implementation
CodebaseOne Git repo, multiple envs
DependenciesComposer-managed
Config.env, injected securely
Backing ServicesRedis, DB, mail via env
Build/Release/RunSeparate deploy steps
ProcessesStateless Laravel workers
Port Bindingartisan serve / FPM
ConcurrencyWorkers with queue:work
DisposabilityFast boot, signal handling
Dev/Prod ParitySail, Forge, and same .env.example
LogsLog to stdout
Admin ProcessesArtisan 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:

These resources provide detailed guidance on each factor and practical examples for modern app development.

Categorized in:

Tagged in: