From .env files to a proper secrets strategy: the 12-factor path
Por Seaworthy · 8 de abril de 2026
Photo by Tanya Barrow on Unsplash
What 12-factor config actually says
Factor III of the 12-factor app is blunt: store config in the environment, not in the codebase. The reasoning is simple. Code goes into source control. Credentials should not. The two must be separated at a level that makes committing a secret physically awkward, not just discouraged by a style guide.
That's it. No ceremony, no specific tooling. The factor doesn't say "use Kubernetes secrets" or "subscribe to a secret manager." It says get config out of the repo. Everything else is implementation detail.
Why does this matter right now? Because AI-generated code almost never gets this right. The model scaffolds a working app, which means DATABASE_URL ends up in a .env that gets committed on the first git add .. I see this constantly in repos that come through analysis. The .env is there, often in the first commit, sometimes with a production Postgres URL that never rotated.
The .env file spectrum
Not all .env files are the same problem. There's a spectrum, and it's worth being precise about where the danger actually lives.
.env in .gitignore, local values only. This is fine. It's the intended use of .env files. Local overrides, dev database credentials, dummy API keys. Nothing real, nothing shared.
.env.example committed with placeholders. Also fine, genuinely useful. Documents what variables the app needs without exposing values.
.env committed with real values. This is the problem. The file is now in git history forever. Rotating the credentials is not enough; someone needs to rewrite history with git filter-repo or accept that anyone with access to the repo had a window.
Credentials baked into a Docker image. Worse. Images get pushed to registries, pulled by CI, cached by build systems. A docker history on the image reveals ENV DATABASE_URL=postgres://... in plain text. The blast radius of this mistake is larger than a committed .env because images get distributed.
Secret managers that don't require an enterprise budget
The gap between "credentials in a text file" and "full enterprise secret management" used to be large. It isn't anymore.
Doppler is the one I reach for first on greenfield projects. Free tier covers one project with multiple environments. You run doppler run -- node server.js and it injects secrets at process start. No .env file anywhere on disk. The CLI pulls from Doppler, your process gets the environment variables, done.
# Install the CLI and authenticate once
doppler login
# Set up for a project
doppler setup
# Run any command with secrets injected
doppler run -- npm start
# Or export for use in shell scripts
export $(doppler secrets download --no-file --format env | xargs)
Infisical is the open-source alternative. Self-hostable if you genuinely cannot put secrets outside your network. The CLI interface is nearly identical to Doppler. Worth knowing about if compliance requirements rule out SaaS.
AWS Secrets Manager makes sense if you're already deep in the AWS ecosystem. The free tier is gone (it costs $0.40/secret/month), but if you're running on EC2 or ECS with an IAM role, the auth story is clean: the instance has permission, you fetch at start, no credentials for credentials.
Runtime injection patterns
For Docker without a secret manager, the minimum viable approach is passing secrets as environment variables at docker run time, sourced from a file that never enters the image:
# .env.production lives on the host, never in the repo or image
docker run --env-file .env.production myapp:latest
For Docker Compose, the same pattern:
services:
app:
image: myapp:latest
env_file:
- .env.production # on the host, not in the repo
For Kubernetes, use Secrets mounted as environment variables. Don't put the actual values in your manifests:
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
The secret object itself gets created via kubectl create secret or, better, via your secret manager's Kubernetes operator. The manifest in your repo contains only a reference to the secret, not the value.
Rotating secrets without downtime
This is where most guides stop and most teams get hurt. You've found a leaked credential. You need to rotate it. How do you do that without taking the app down?
The pattern is dual-read with an overlap window:
- Generate the new credential.
- Add it to your secret manager as a second entry (
DATABASE_URL_NEW, or version the secret). - Deploy a code change that accepts both the old and new credential (for database passwords, this usually means adding the new user in the database, not replacing the old one).
- Verify traffic is flowing correctly.
- Remove the old credential from the secret manager and from the database.
- Deploy again to clean up the dual-read logic.
For API keys this is simpler: most providers allow multiple active keys. Create the new one, deploy with it, revoke the old one. Steps 3-6 collapse into two.
The window between step 2 and step 5 is your safety net. Don't rush it. An hour of overlap costs nothing. A botched rotation that drops traffic costs significantly more.
I genuinely don't know how to feel about the fact that most teams skip this entirely. They rotate the credential, update the secret in one place, hope nothing breaks mid-deploy, and find out the hard way that their container was cached with the old value. Set the overlap window. It takes ten minutes to plan and saves a genuinely bad on-call shift.
Seaworthy flags committed .env files and credentials baked into Dockerfiles automatically.
Este artículo fue generado por IA y resume fuentes públicamente disponibles.
Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.
Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.