Environment Variables Explained — What They Are and How to Use Them
What environment variables are, why hardcoding credentials destroys security, how to use .env files correctly, how to set variables in production, and a complete security checklist.
If you have ever seen .env files, process.env, or os.environ in a codebase and wondered what they are for, this guide explains everything. Environment variables are one of the most fundamental concepts in professional software development, and getting them wrong is one of the most common ways developers accidentally expose credentials and break production deployments.
What Is an Environment Variable?
An environment variable is a key-value pair stored outside of your application code that your application reads at runtime. The word "environment" refers to the operating system environment in which your process runs: a set of named values that are available to any program that asks for them.
The key is always written in uppercase with underscores between words (a convention called SCREAMING_SNAKE_CASE). The value is whatever that variable should contain: a URL, an API key, a port number, a flag like true or false, or any string your application needs to behave correctly in a specific environment.
The critical distinction is that these values live outside your codebase. They are not in any JavaScript file, Python module, or configuration file that gets committed to Git. They exist only in the runtime environment where your application executes.
An environment variable is a named value your application reads from its runtime environment rather than from its source code. The same binary can behave differently in development, staging, and production by reading different variable values.
Why Not Just Put These Values in Your Code?
The naive approach is to write configuration values directly in source code. It is tempting because it is simple and it works immediately. But it introduces three serious problems that reliably cause security incidents and deployment failures in production systems:
Removing a credential from the current version of your code does not remove it from Git history. Rotating it (revoking the old key and generating a new one with the service that issued it) is the only safe response. Removing the commit from history is complex and does not help if anyone has already cloned or forked the repository.
How Environment Variables Work
Your operating system maintains a set of environment variables for each running process. When a new process starts, it inherits a copy of the environment from its parent process. Your application reads from this inherited environment at startup or on demand.
Setting Variables in the Terminal
Reading Environment Variables in Your Code
Every major language and runtime has a built-in way to read environment variables. The pattern is always the same: read the value by name, provide a default for optional variables, and validate that required variables are present at startup.
If a required environment variable is missing and your code only reads it when that feature is exercised, the application will crash in a confusing way at runtime, potentially in the middle of a user action. Instead, write a startup validation block that checks all required variables immediately when the application boots. This produces a clear, immediate error message at startup and prevents deployment of misconfigured applications.
The .env File: Local Development Made Simple
Setting environment variables manually in the terminal every session is impractical. For local development, the standard solution is a .env file: a plain text file in the project root containing key-value pairs that a library automatically loads into the environment at application startup.
Loading .env in Node.js with dotenv
Loading .env in Python with python-dotenv
Both the Node.js dotenv and Python python-dotenv libraries follow a safe convention: they do not overwrite environment variables that are already set in the environment. This means that if your hosting platform sets DATABASE_URL, your local .env file will not override it when deployed. The same code works correctly in both local development (reading from .env) and in production (reading from the platform's configured environment).
The Most Important Rule: .env Must Be in .gitignore
.env file contains real credentials. Once committed, those credentials exist in every clone, fork, and backup of the repository permanently. Even a private repository is a risk: team members change, access is granted and revoked, repositories are occasionally made public. The correct approach is to never let secrets touch version control at all.Run git check-ignore -v .env after adding the entry. If the command prints the .gitignore rule that matched, the file is correctly ignored. If it prints nothing, the file is not ignored and will be committed on the next git add .. Check this every time you set up a new project.
.env.example: The File You Do Commit
Every project that uses environment variables should have a .env.example file committed to the repository. This file has exactly the same keys as your real .env file but contains no real values: only placeholder descriptions of what each variable should contain.
When a new developer clones the repository, they copy .env.example to .env and fill in the real values for their local environment. The example file communicates exactly which variables the application needs, what format they should be in, and whether to use test or live keys, without exposing any real credentials.
Every time a developer adds a new environment variable to the application, they must also add the corresponding key with a placeholder value to .env.example. Make this a required step in your code review process. A stale .env.example that is missing required variables breaks new developer onboarding and causes confusing startup errors. Use the Text Diff Checker to compare your .env and .env.example files periodically to confirm they have the same set of keys.
Environment-Specific Configuration
Professional applications run in multiple environments: at minimum development (your local machine), staging (a production-like test environment), and production (the live system). The same application code runs in all three. The behaviour differs entirely because different variable values are supplied in each environment.
| Variable | Development | Staging | Production |
|---|---|---|---|
| DATABASE_URL | localhost:5432/myapp_dev | staging-db.example.com/myapp | prod-db.example.com/myapp |
| NODE_ENV | development | staging | production |
| STRIPE_SECRET_KEY | sk_test_xxx (test key) | sk_test_xxx (test key) | sk_live_xxx (live key) |
| LOG_LEVEL | debug | info | error |
| EMAIL_ENABLED | false | false | true |
| CACHE_TTL | 0 (disabled) | 60 seconds | 3600 seconds |
| CORS_ORIGIN | http://localhost:3000 | https://staging.example.com | https://example.com |
Notice that Stripe test keys are used in both development and staging: this prevents any real charges during testing. Email sending is disabled in development and staging to avoid sending real emails during development. Log level is set to debug in development (verbose output for debugging) and error in production (only critical failures logged, to reduce log volume and avoid leaking sensitive data into logs).
Setting Environment Variables in Production
Local .env files are for development only. In production, environment variables are set through your hosting platform's interface. Each platform has its own method, but the principle is the same: variables are configured in the platform's dashboard or CLI, stored securely by the platform, and injected into your application's environment at runtime.
Both Docker Compose and Kubernetes manifest files are often committed to version control for infrastructure-as-code purposes. If you include actual secret values in these files, those secrets are in your repository. Use environment variable references (${MY_SECRET}) in compose files and Kubernetes Secrets objects for sensitive values, keeping the actual credentials out of any committed file.
Naming Conventions for Environment Variables
Consistent naming makes environment variables easy to scan, search, and document. The conventions below are widely adopted across the industry and appear in virtually every professional codebase:
- DATABASE_URL
- STRIPE_SECRET_KEY
- SENDGRID_API_KEY
- TWILIO_ACCOUNT_SID
- JWT_SECRET
- REDIS_HOST
- S3_BUCKET_NAME
- database_url (lowercase)
- stripekey (no separator)
- SK (not descriptive)
- MY_KEY (too generic)
- key1 (numbered, meaningless)
- DatabaseURL (mixed case)
- secret (no context)
The prefix-by-service pattern (STRIPE_, SENDGRID_, TWILIO_) is particularly valuable: it groups all variables for a service together when sorted alphabetically, makes it immediately clear which service each credential belongs to, and makes it easier to audit which services an application integrates with. Use the Case Converter to quickly convert any name to SCREAMING_SNAKE_CASE when adding new variables.
The Environment Variable Security Checklist
Run through this checklist for every project before it goes anywhere near production. Each item represents a class of real-world security incident that has caused data breaches and credential leaks:
git check-ignore -v .envgit log -S "sk_live"7-Step Workflow: Setting Up Environment Variables in a New Project
Follow this sequence every time you start a new project or add environment variable support to an existing one. Doing these steps in order prevents the most common mistakes:
- Create .gitignore before creating .env. Add .env and all .env.* variants to your .gitignore file as the very first step, before the .env file even exists. This prevents any possible window where the file could be accidentally staged. Verify with git check-ignore -v .env immediately afterward.
- Create the .env file with development values. Now create your .env file with real development values: your local database URL, test API keys, a development JWT secret. Use test-mode keys from every external service: Stripe test keys, sandbox API credentials. Never put live keys in a local .env file.
- Install and configure the dotenv library. Install dotenv (Node.js) or python-dotenv (Python). Add the load call at the very top of your application entry point, before any other imports that might read environment variables. Confirm variables are accessible by logging one non-sensitive value at startup.
- Add startup validation for required variables. Write a startup check that reads every required environment variable and throws an informative error if any are missing. This prevents your application from starting in a misconfigured state and produces a clear error message that tells the developer exactly which variable is absent.
- Create .env.example with placeholder values. Copy your .env file to .env.example. Replace every real value with a descriptive placeholder: your_database_url_here, sk_test_your_stripe_key. Add a comment above each variable explaining what it is and where to get it. Commit this file to the repository.
- Document the setup process in your README. Add a "Getting Started" section to your README that tells new developers to copy .env.example to .env and fill in their values. Link to where credentials can be obtained (the Stripe dashboard, the SendGrid account, the internal credentials store). This makes onboarding deterministic rather than requiring institutional knowledge.
- Configure production environment variables in the hosting platform. Log in to your hosting platform (Vercel, Heroku, Netlify, AWS, etc.) and add all required variables with their production values. Use live API keys, production database URLs, and strong random secrets for JWT and session tokens. Use the Slug Generator as a quick source of random strings for secrets that do not need a specific format.
Frequently Asked Questions About Environment Variables
NODE_ENV is a conventional environment variable that tells a Node.js application which environment it is running in. The three standard values are development, test, and production. Many libraries change their behaviour based on this value: Express.js disables detailed error pages in production, React builds a smaller optimised bundle, webpack enables minification. Setting NODE_ENV=production in production is a performance and security requirement for Node.js applications, not just a label. Some libraries perform significantly worse or expose debug information if this value is not set correctly.
Yes, but with a critical restriction: environment variables in a frontend bundle are baked into the compiled JavaScript that is sent to the browser. Anyone can inspect them. This means you must never put server-side secrets (API keys with write access, database passwords, JWT secrets) in frontend environment variables. Only put values that are safe to be publicly visible, such as a public Stripe publishable key or a public analytics measurement ID. In Create React App and Vite, variables that should be included in the bundle must be prefixed with REACT_APP_ or VITE_ respectively. Variables without this prefix are not included in the bundle, which prevents accidental exposure of sensitive variables.
Act immediately: the credential is compromised from the moment it appears in a commit, regardless of whether anyone has seen it. First, revoke the exposed credential through the service that issued it: go to your Stripe dashboard, AWS console, SendGrid account, or wherever the key came from, and revoke or regenerate it immediately. Second, generate a new credential and deploy it to your production environment. Third, update your .gitignore to prevent recurrence. Attempting to rewrite Git history to remove the commit is complex, error-prone, and does not help if anyone has already cloned or forked the repository before you rewrote it. Rotating the credential is always the priority.
Shell environment variables (set with export KEY=value) exist only for the current terminal session and its child processes. When the terminal is closed, they are gone. They also apply to every process started from that shell, which can cause unexpected behaviour if two projects need different values for the same variable name. A .env file is project-specific: it is loaded only when that project's application starts, it persists across terminal sessions, and it is version-controllable (via .env.example). For development, .env files are consistently preferable to shell exports because they are project-scoped and self-documenting.
Never share .env files over email, Slack, or any other communication platform that stores message history. Use a dedicated secrets management tool: 1Password Teams, Doppler, Hashicorp Vault, or AWS Secrets Manager all support sharing secrets with access controls and audit logs. For smaller teams, a password manager with shared vault support (1Password, Bitwarden) is a practical starting point. The key requirements are access control (only team members who need a secret can see it), audit logging (you can see who accessed a secret and when), and rotation support (you can update a secret in one place and the change propagates). Never share secrets through any channel that cannot be audited or revoked.
Secrets (API keys, database passwords, JWT secrets) must always be environment variables. Non-sensitive configuration that changes between environments (database host, log level, feature flags, service URLs) should also typically be environment variables because they need different values per environment. Configuration that is the same across all environments and is not sensitive (default page size, UI colour themes, application name) can reasonably live in configuration files committed to the repository. A useful heuristic: if a value would need to change when you deploy to a different environment, or if you would not want it visible in your public repository, it belongs in an environment variable.
Tools for working with configuration and data
Convert variable names to SCREAMING_SNAKE_CASE, compare .env files between environments, format JSON config data, and more. All free, all in your browser, no login required.
Keep Secrets Out of Code. Keep Configuration Out of the Binary.
Environment variables solve two problems at once: they keep credentials out of version control where they could be exposed, and they allow the same application code to behave correctly in development, staging, and production without modification. Both benefits are fundamental to how professional applications are built and deployed.
The three rules that prevent the majority of environment variable mistakes are: always add .env to .gitignore before the file exists, always commit a .env.example with placeholder values, and always use the hosting platform's native environment configuration in production rather than deploying .env files to servers. Follow those three rules and the security checklist in this guide and you will avoid the class of incidents that has exposed credentials for some of the most well-known companies in the industry.
Use the Case Converter to keep variable names consistently in SCREAMING_SNAKE_CASE, and the Text Diff Checker to compare your .env and .env.example files to confirm they stay in sync as your project grows.