How to Manage Environment Variables in Web Applications

Terminal screen showing environment variable configuration and shell commands

Every web application needs configuration: database connection strings, API keys, third-party service credentials, feature flags, and a dozen other values that change between environments. How you manage that configuration determines whether deploying to a new environment takes ten minutes or three days, and whether your credentials end up in a public repository at 2 a.m.

Environment variables are the standard approach, but "use environment variables" is the beginning of the answer, not the whole thing. A poorly designed environment variable setup trades one set of problems for another. This guide covers how to structure it correctly, from local development through CI/CD and production containers.

What Environment Variables Actually Are

An environment variable is a name-value pair that exists in the process environment, set before the application starts. The operating system passes these values to the process, where the application reads them through the standard runtime APIs: process.env.DATABASE_URL in Node.js, os.environ['DATABASE_URL'] in Python, System.getenv("DATABASE_URL") in Java.

The key property is that the value is not in the code. The code references the variable name; the value lives in the environment where the code runs. This means the same codebase can connect to a local database in development and a production database in production without any code changes.

Wikipedia's entry on environment variables covers the POSIX standard definition and the history of how this pattern became universal across operating systems and runtimes.

terminal screen close monospace
Photo by Ec lipse on Pexels

Why Configuration Varies Across Environments

Most applications run in at least three environments: development (your local machine), staging (a production-like test environment), and production. Each environment needs different values for the same configuration keys:

Key Development Staging Production
DATABASE_URL localhost:5432/myapp_dev staging.db.internal/myapp prod.db.internal/myapp
LOG_LEVEL debug info error
STRIPE_SECRET_KEY sktest... sktest... sklive...

These values cannot be in the codebase because checking them in means committing production credentials to a repository accessible to every developer and every CI/CD runner. The first time that repository accidentally becomes public, the incident is not theoretical.

Environment variables solve this by putting the values outside the code entirely. Each environment has its own set of values, and the code never needs to know which environment it's running in. If the code needs environment-specific behavior, it reads a NODE_ENV or APP_ENV variable rather than having if (isDevelopment) branches spread through business logic.

Using .env Files Without Getting Into Trouble

In local development, manually setting environment variables before every terminal session is impractical. The convention is to use a .env file in the project root: a plain text file with one KEY=VALUE pair per line that a library reads at startup.

DATABASE_URL=postgresql://localhost:5432/myapp_dev
LOG_LEVEL=debug
STRIPE_SECRET_KEY=sk_test_abc123

Libraries like dotenv for Node.js and similar packages for Python, Ruby, and Go load this file automatically during development. The key rule: .env must be in .gitignore. The file is for local development only. It should never be committed to the repository.

What should be committed is a .env.example file with the same keys but no real values, just placeholder descriptions:

DATABASE_URL=postgresql://hostname/dbname
LOG_LEVEL=debug|info|error
STRIPE_SECRET_KEY=sk_test_...

This documents what variables the application needs without exposing any secrets. New developers clone the repository, copy .env.example to .env, fill in their local values, and the application works. This pattern also forces maintainers to keep .env.example current whenever a new required variable is added, which serves as living documentation of the application's configuration surface.

Separating Secrets From Configuration

Not all environment variables are equally sensitive. It helps to think in two buckets:

Secrets are values that, if exposed, allow someone to do something harmful: database passwords, API keys with write access, signing keys for tokens, encryption keys. These must be handled with explicit security controls. Rotating them when a developer leaves the team is a real operational task, not just a best practice on a checklist.

Configuration is everything else: log level, cache TTL, feature flags, base URLs for third-party services. These are not sensitive on their own, though leaking them still reveals implementation details about the system architecture.

Mixing these categories together is fine in small applications. As systems grow, separating secrets into a dedicated secrets manager and keeping non-sensitive configuration in environment variables or a config file gives you better audit trails and rotation tooling for the values that need it.

For most web applications, a practical threshold is: if the value would let an attacker access a paid service, modify data, or impersonate a user, it's a secret. Everything else is configuration.

server rack cables organized
Photo by Field Engineer on Pexels

"The leak pattern I see most often is a developer who wants to debug something in production, adds console.log(process.env) without thinking, and forgets to remove it. All your secrets are now in the logs, potentially in a third-party logging service. Mask secrets at the injection point, not by hoping nobody logs them." - Dennis Traina, founder of 137Foundry

Environment Variables in Containers and CI/CD

Modern applications deploy via containers (Docker), orchestrators, or CI/CD pipelines. Each has its own mechanism for injecting environment variables.

Docker: Pass variables at runtime with -e flags or a --env-file option:

docker run -e DATABASE_URL=postgres://... -e LOG_LEVEL=info myapp

Never hardcode values in the Dockerfile. A Dockerfile with ENV DATABASE_URL=... bakes the value into the image. Container images are frequently pushed to registries and shared; anything in the image layer history is potentially visible to anyone with access to the image.

Docker Compose: Use an env_file directive pointing to the .env file, or inline the environment block. The same rule applies: production secrets should not appear in files committed to the repository. The docker-compose.yml file defines which env file to load; the secrets themselves stay in the untracked .env.

CI/CD Pipelines: Most CI/CD platforms (GitHub Actions, GitLab CI, CircleCI) have a secrets or environment variable store in their UI. Set sensitive values there, not in pipeline configuration files. Pipeline configuration files end up in the repository; the secrets store does not. Reference secrets in pipeline files using the platform's variable syntax, which resolves the value at runtime without writing it into the file.

A useful mental model from The Twelve-Factor App, a widely referenced methodology for modern application design: configuration is everything that varies between deployments. The twelve-factor approach prescribes storing all such configuration in the environment, keeping config entirely separate from code.

The Validation Problem: Failing Early

Applications that read environment variables lazily, checking for a value only when it's first needed, produce confusing failures. A missing DATABASE_URL might cause an error only when the first database query runs, deep in the application's startup sequence, with a stack trace that doesn't obviously indicate a configuration problem.

The fix is to validate all required environment variables at startup, before the application begins listening for requests. If any required variable is missing or obviously wrong, the process should exit immediately with a clear error message:

const required = ['DATABASE_URL', 'STRIPE_SECRET_KEY', 'SESSION_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
  console.error(`Missing required environment variables: ${missing.join(', ')}`);
  process.exit(1);
}

This pattern surfaces configuration problems immediately rather than hiding them behind runtime errors. The application either starts fully configured or refuses to start entirely. For containers in a health-check-based orchestration system, a startup failure that exits cleanly is preferable to an application that starts, enters a half-working state, and produces cryptic errors for minutes before someone diagnoses the root cause.

data center hallway servers
Photo by Christina Morillo on Pexels

Avoiding Common Mistakes

Committing .env files. This is the most common and most damaging mistake. Add .env to .gitignore immediately when setting up a project, before any secrets ever appear in the file. Several significant credential leaks trace back to a .env file that was committed once early in a project's history and then forgotten.

Logging environment variables in full. Many frameworks and error reporters dump all environment variables when a crash occurs. Configure your logger to mask or omit any key that ends in _KEY, _SECRET, _TOKEN, or _PASSWORD. Libraries like pino in Node.js support redaction rules that handle this automatically.

Sharing the same credentials across environments. Using a production API key in staging means a staging incident can become a production incident. Each environment should have its own set of credentials with the minimum access it actually needs for its purpose.

Checking for variable existence but not validity. An application that verifies process.env.PORT exists but doesn't validate that it's a number will start successfully and fail in a harder-to-debug way when it tries to bind a port with a string value. For critical variables, validate type and format at startup, not just presence.

OWASP publishes guidance on secure configuration management that covers these and related patterns in more depth, including the risks of storing sensitive data in environment variables versus a dedicated secrets manager.

How This Fits Into a Larger Application Architecture

Environment variable management is one piece of the broader configuration management problem. 137Foundry's approach to web application development builds this into the initial architecture, so it doesn't have to be retrofitted later when the configuration surface has grown. A system that starts with clean configuration separation is much easier to audit and rotate than one where credentials were added ad hoc over several years.

For applications with complex environment management needs, including multi-region deployments or strict compliance requirements, the 137Foundry web development service covers infrastructure setup as part of the engagement. More information about related capabilities is on the services hub.

fiber optic light strands
Photo by Marek Piwnicki on Pexels

Summary

Good environment variable management comes down to a few consistent practices: keep secrets out of the repository using .gitignore, validate at startup before accepting requests, use separate credentials per environment, and treat the .env file as a local development tool that never touches source control.

The Twelve-Factor App methodology at 12factor.net formalizes this and related practices into a checklist worth reading once before starting any new project. Most modern deployment best practices for web applications trace back to those twelve factors in some form.

None of these rules are complex individually. The difficulty is maintaining them consistently as a team grows and the application's configuration surface expands. The earlier a project establishes clear conventions, the less retrofitting happens later when the stakes are higher.

Need help with Web Development?

137Foundry builds custom software, AI integrations, and automation systems for businesses that need real solutions.

Book a Free Consultation View Services