Environment Variables Explained: Why You Need .env Files

Posted November 29, 2025 by Karol Polakowski

Environment variables are a simple but powerful mechanism for configuring applications without changing code. They let you keep secrets and environment-specific settings outside your repository, follow the 12‑factor app principles, and swap configuration between local development, CI, and production quickly.

What environment variables are (and why they matter)

Environment variables are key/value pairs provided to a process by its runtime environment. They are used to store configuration such as database connection strings, API keys, feature flags, and runtime toggles. Using environment variables brings several advantages:

  • Separation of config from code — the same artifact can run in different environments without rebuilds.
  • Reduced risk of accidentally committing secrets into version control.
  • Clearer deployment and automation via CI/CD, Docker, and orchestration platforms.
  • Easier secret rotation and centralized management when paired with secret stores.

The .env file pattern

A .env file is a text file that contains environment variables in KEY=VALUE format. It’s typically used for local development and CI that reads env files. Example:

# .env (local development only)
DB_HOST=localhost
DB_USER=dev_user
DB_PASS=localpassword
API_URL=http://localhost:3000
FEATURE_X=true

Common conventions:

  • .env — base local settings (do not commit secrets)
  • .env.local — local overrides (ignored by git)
  • .env.example (or .env.sample) — non-sensitive template committed to the repo
  • .env.production — production-specific values (usually not committed)

Add .env and .env.local to .gitignore and commit .env.example to document required variables:

# .gitignore
.env
.env.local

Loading .env in Node.js

For local Node development, the popular dotenv package loads .env into process.env. Minimal usage:

// index.js
require('dotenv').config();
const conn = `postgres://${process.env.DB_USER}:${process.env.DB_PASS}@${process.env.DB_HOST}/appdb`;
console.log('DB host', process.env.DB_HOST);

Notes:

  • dotenv is for development convenience. In production, inject variables via the hosting platform, Docker, or a secrets manager.
  • Calling config() multiple times overwrites only undefined values by default.

Next.js and browser-exposed variables

Frameworks that render both server and client have rules for exposing env vars to the browser. In Next.js:

  • Prefix client-accessible variables with NEXT PUBLIC . Example: NEXT PUBLIC API_URL
  • Server-only variables (e.g., database passwords) must not have that prefix.

Example usage in a React component:

// components/ApiClient.js
export const apiUrl = process.env.NEXT_PUBLIC_API_URL;

In production on Vercel or other hosts, define these variables in the project settings or environment configuration UI instead of using .env files.

Docker, Docker Compose, and Kubernetes

Docker can accept env vars via Dockerfile ENV, docker run -e, or docker-compose env_file. For compose:

# docker-compose.yml (snippet)
services:
  web:
    build: .
    env_file:
      - .env
    ports:
      - "3000:3000"

Kubernetes uses Secrets and ConfigMaps. Do not mount raw .env files into production clusters; prefer Kubernetes Secrets (base64-encoded) or external secret stores.

Security considerations and best practices

  • Never commit secret values. Use .env.example to document keys without values.
  • Treat .env files as local development convenience only. Use managed secret stores in production: AWS Secrets Manager, Parameter Store, HashiCorp Vault, Google Secret Manager.
  • Avoid printing secrets to logs.
  • Limit process.env access in client-side bundles. Ensure only intended values are exposed.
  • Rotate and audit secrets regularly. Use short-lived tokens where possible.
  • For typed languages or TypeScript, declare a typed interface or use dotenv-safe/Schema validation to ensure required variables exist.

Example .env.example and dotenv-safe pattern

Commit a .env.example to illustrate required variables. Use a validator (dotenv-safe or a small startup check) so the app fails fast when required keys are missing.

# .env.example
DB_HOST=
DB_USER=
DB_PASS=
API_URL=

Startup check example (Node):

// config.js
const required = ['DB_HOST', 'DB_USER', 'DB_PASS'];
const missing = required.filter(k => !process.env[k]);
if (missing.length) throw new Error(`Missing env vars: ${missing.join(', ')}`);

When not to use .env files

  • Production secret management and rotation — use a secret manager.
  • Complex multi‑service infrastructure — use orchestration features (Kubernetes Secrets, AWS IAM roles, etc.).
  • If your deployment platform already provides a secure environment variable store, rely on that instead of shipping .env files.

Practical checklist

  • Add .env and .env.local to .gitignore.
  • Commit .env.example with the required keys and example (non-sensitive) values.
  • Use dotenv or framework integrations only for local development and CI convenience.
  • Keep secrets out of client bundles; prefix client vars intentionally (e.g., NEXT PUBLIC ).
  • Use secret managers in production and rotate secrets regularly.

Conclusion

.env files are a lightweight and developer-friendly way to manage configuration for local development. They align with the 12‑factor principle of separating config from code, but they are not a production-grade secret store. Use them for convenience, document required variables with .env.example, and rely on platform-native secret management for production workloads.