Menu

Security Configuration Best Practices Beginner Guide March 2026 ⏱ 12 min read

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.

.env format
DATABASE_URL=postgresql://user:password@localhost:5432/myapp API_KEY=sk_live_abc123xyz789... NODE_ENV=production PORT=3000 JWT_SECRET=a_long_random_string_here STRIPE_SECRET_KEY=sk_live_51NxYz...

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:

🔓
Credentials in Git History Forever
Source code gets committed to Git. Once an API key or database password appears in a commit, it exists in the repository history permanently. Even after you delete it from the current code, it remains in every previous commit. Anyone with repository access, including anyone who ever forked it, has your credentials.
⚙️
Different Environments Need Different Values
Your local database URL is localhost:5432. Your production database is on a remote server. Your test API key is sk_test_xxx. Your live key is sk_live_xxx. Hardcoded values mean you either need different code per environment or you risk connecting production to the wrong service entirely.
🌐
Secrets Can Reach the Client Side
In modern JavaScript frameworks, code is often bundled and shipped to the browser. Server-side secrets hardcoded in the wrong place can end up in client-side bundles that any user can inspect. API keys exposed in browser JavaScript are compromised immediately.
🚨 If a secret has ever been committed to Git, treat it as compromised

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.

1
The OS or deployment platform sets variables
Variables are set in the terminal, in a .env file loaded at startup, or in the hosting platform's configuration dashboard. They exist in the process's environment memory.
2
Your application process starts
When Node.js, Python, or any other runtime launches your application, it inherits all environment variables set in the parent environment.
3
Your code reads the variables at runtime
Calls to process.env.DATABASE_URL or os.environ.get('DATABASE_URL') read the value from the inherited environment. The value is never in any source file.
4
Different environments supply different values
Development, staging, and production each supply different variable values. The code is identical in all three. Only the configuration differs.

Setting Variables in the Terminal

bash / macOS / Linux
# Temporary: only for the current session export DATABASE_URL="postgresql://localhost/myapp" export API_KEY="sk_live_abc123" # Inline: only for a single command DATABASE_URL=postgresql://localhost/myapp node server.js # Permanent: add to ~/.bashrc or ~/.zshrc echo 'export API_KEY="sk_live_abc123"' >> ~/.zshrc

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.

Node.js process.env
// Reading variables const dbUrl = process.env.DATABASE_URL; const port = process.env.PORT || 3000; // Validate required variables at startup const required = ['DATABASE_URL', 'JWT_SECRET']; required.forEach(key => { if (!process.env[key]) { throw new Error(`Missing: ${key}`); } });
Python os.environ
import os # .get() returns None if not set db_url = os.environ.get('DATABASE_URL') port = int(os.environ.get('PORT', 3000)) # Raise if required variable is missing api_key = os.environ['API_KEY'] # KeyError if API_KEY not set
Ruby ENV
# Reading variables db_url = ENV['DATABASE_URL'] port = ENV.fetch('PORT', '3000').to_i # fetch raises KeyError if missing api_key = ENV.fetch('API_KEY')
Go os.Getenv
import "os" // Returns "" if not set dbURL := os.Getenv("DATABASE_URL") // LookupEnv returns ok=false if missing key, ok := os.LookupEnv("API_KEY") if !ok { log.Fatal("API_KEY not set") }
⚠ Validate required variables at application startup, not when they are first used

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.

.env (local development only — never commit this)
# Database DATABASE_URL=postgresql://localhost:5432/myapp_dev DATABASE_POOL_SIZE=5 # Authentication JWT_SECRET=dev_secret_change_in_production JWT_EXPIRES_IN=7d # External APIs (use test keys in development) STRIPE_SECRET_KEY=sk_test_51NxYz... SENDGRID_API_KEY=SG.test_key_here # Application NODE_ENV=development PORT=3000 LOG_LEVEL=debug

Loading .env in Node.js with dotenv

Node.js
// Install: npm install dotenv // At the very top of your entry point (e.g. index.js, server.js) require('dotenv').config(); // All .env variables are now available in process.env console.log(process.env.DATABASE_URL); console.log(process.env.PORT); // With ES Modules (import syntax) import 'dotenv/config';

Loading .env in Python with python-dotenv

Python
# Install: pip install python-dotenv from dotenv import load_dotenv import os # Load .env into os.environ at startup load_dotenv() # Variables are now available db_url = os.environ.get('DATABASE_URL') api_key = os.environ.get('API_KEY')
✅ dotenv only loads variables that are not already set

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

Critical security rule
Your .env file must never be committed to Git. Add it to .gitignore the moment you create it.
The .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.
.gitignore — add these lines immediately
# Environment variable files — never commit these .env .env.local .env.development .env.staging .env.production .env.*.local # Verify it is ignored before committing: # git check-ignore -v .env # Expected output: .gitignore:1:.env .env

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.

🔴 .env — never commit
# Real credentials — private DATABASE_URL=postgresql://admin:s3cr3t@prod-db.example.com/app JWT_SECRET=xK9mP2nL7qR4tW8v... STRIPE_SECRET_KEY=sk_live_51NxYzABC... SENDGRID_API_KEY=SG.realkey123abc...
✅ .env.example — safe to commit
# Copy to .env and fill in real values DATABASE_URL=postgresql://localhost:5432/yourapp JWT_SECRET=your_jwt_secret_here STRIPE_SECRET_KEY=sk_test_your_key_here SENDGRID_API_KEY=your_sendgrid_key_here

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.

💡 Keep .env.example updated as a team discipline

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.

🔷
Vercel
Dashboard UI
Project Settings → Environment Variables. Set per-environment (Development, Preview, Production). Variables are encrypted at rest.
🟢
Netlify
Dashboard UI
Site Configuration → Environment Variables. Supports different values per deployment context (production, branch deploys, dev).
🟣
Heroku
CLI or Dashboard
heroku config:set KEY=value. View all with heroku config. Variables are called "Config Vars" in Heroku terminology.
🟠
AWS
Secrets Manager
Use AWS Secrets Manager or Parameter Store for credentials. Lambda functions support environment variables directly in the function configuration.
🐳
Docker
-e flag / compose
Pass with docker run -e KEY=value or define in docker-compose.yml under environment:. Never hardcode in the Dockerfile itself.
☸️
Kubernetes
Secrets / ConfigMaps
Use Kubernetes Secrets for sensitive values and ConfigMaps for non-sensitive configuration. Mount as environment variables in the pod spec.
⚠ Never put secrets in docker-compose.yml or Kubernetes manifests committed to Git

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:

✅ Good naming
  • DATABASE_URL
  • STRIPE_SECRET_KEY
  • SENDGRID_API_KEY
  • TWILIO_ACCOUNT_SID
  • JWT_SECRET
  • REDIS_HOST
  • S3_BUCKET_NAME
✗ Avoid these patterns
  • 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:

Security checklist — verify all items before deploying
Environment Variable Security Review
.env is in .gitignore. Verify with: git check-ignore -v .env
.env.example with placeholder values is committed and up to date
No API keys, passwords, or secrets appear anywhere in source code files
Git history searched for accidental credential commits: git log -S "sk_live"
Production uses the hosting platform's environment configuration, not a deployed .env file
Test/sandbox API keys used in development and staging, never live keys
Client-side bundles (React, Vue, Angular) do not contain server-side secrets
Required variables validated at application startup with a clear error on missing values
Secrets are rotated if they have ever been accidentally committed or shared
docker-compose.yml and Kubernetes manifests do not contain real credential values
Application logs never print full variable values (use partial masking if needed)
Access to production environment variables restricted to the team members who need it

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

What is NODE_ENV and why does it matter?

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.

Can I use environment variables in a frontend React or Vue application?

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.

What should I do if I accidentally committed a .env file or an API key to Git?

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.

What is the difference between a .env file and environment variables set in the shell?

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.

How do I share environment variables securely with my team?

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.

Should all configuration values be environment variables, or just secrets?

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.

Free browser-based developer tools

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.