Ctrl + K
Environment23 min read

dotenv Best Practices

Practical dotenv best practices for organizing .env files, naming environment variables, managing multiple environments, protecting secrets, validating configuration and avoiding common mistakes in modern web applications.

Published: 2026-10-05

The dotenv package and .env files make it easy to provide configuration to an application without hard-coding values into source code. Database URLs, API keys, feature flags, ports and environment-specific settings can all be supplied through environment variables.

However, simply using dotenv does not automatically make configuration secure or well organized. Problems usually appear when projects accumulate multiple .env files, inconsistent variable names, duplicated values, accidentally committed secrets or configuration that behaves differently between development and production.

This guide covers practical dotenv best practices for Node.js and modern JavaScript applications, including file organization, naming conventions, secret management, validation, environment separation, testing, CI/CD and production deployment.

What Is dotenv?

dotenv is a popular Node.js package that loads variables from a .env file into process.env. A typical .env file contains simple key-value pairs.

PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp
API_BASE_URL=https://api.example.com

After loading the file, the application can access these values through process.env.

import "dotenv/config";

console.log(process.env.PORT);
console.log(process.env.DATABASE_URL);

The important distinction is that dotenv is a configuration-loading mechanism, not a secret-management system. It does not encrypt your .env file, rotate credentials or prevent secrets from being committed to Git.

1. Never Commit Real .env Files to Git

One of the most important dotenv practices is keeping real environment files out of version control. A .env file can contain database passwords, private API keys, access tokens and other credentials.

.env
.env.local
.env.*.local

The exact ignore rules depend on the framework and project structure, but the principle is simple: credentials belonging to a specific machine or deployment should not be stored in the repository.

⚠️ Adding .env to .gitignore does not remove a file that has already been committed. If a secret has entered Git history, treat it as potentially exposed and rotate the credential rather than relying only on deleting the file in a later commit.

2. Commit a .env.example File

While real .env files should normally remain private, projects benefit from documenting which variables are required. A .env.example file is a convenient way to describe the configuration without exposing real credentials.

PORT=3000
DATABASE_URL=
API_KEY=
API_BASE_URL=http://localhost:3000
LOG_LEVEL=info

Developers can copy the example file and provide their own values locally. This makes onboarding easier and reduces the chance that somebody has to guess which environment variables are required.

💡 Keep .env.example synchronized with the application's actual configuration. An outdated example file can be almost as confusing as having no documentation at all.

3. Use Clear and Consistent Variable Names

Environment variables become difficult to maintain when naming conventions change throughout a project. Choose one predictable convention and use it consistently.

DATABASE_URL=...
DATABASE_HOST=...
DATABASE_PORT=5432
DATABASE_NAME=...

REDIS_URL=...
REDIS_HOST=...
REDIS_PORT=6379

API_BASE_URL=...
API_TIMEOUT_MS=10000

Uppercase names with underscores are a common convention for environment variables. Prefixes can also group related configuration, such as DATABASE_, REDIS_, API_ or SMTP_.

Avoid ambiguous names such as URL, TOKEN or KEY when the project contains several values of the same type. Names such as PAYMENT_API_KEY and ANALYTICS_API_KEY communicate their purpose much more clearly.

4. Separate Configuration by Environment

Development, testing, staging and production rarely use exactly the same configuration. A local application might connect to a local database, while production uses a managed database and different credentials.

Many ecosystems support environment-specific files. Depending on the framework, common names include .env, .env.local, .env.development, .env.test and .env.production.

.env
.env.local
.env.development
.env.test
.env.production

The exact loading order and precedence are framework-specific, so do not assume that dotenv itself defines the behavior of every file name. Framework documentation should be treated as the source of truth when several files are supported.

5. Keep Local Overrides Separate

A useful pattern is to keep shared, non-secret defaults separate from machine-specific values. This prevents every developer from modifying the same configuration file simply to use a different local database or API endpoint.

# .env
NODE_ENV=development
LOG_LEVEL=debug
API_TIMEOUT_MS=10000
# .env.local
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=local-development-key

Whether .env.local is appropriate depends on the framework. The broader principle is to keep personal overrides separate from configuration that should be shared by the project.

6. Do Not Put Secrets in Source Code

Moving a secret from one JavaScript file into a .env file is useful, but the application should also respect the server-client boundary. A value loaded from process.env is not automatically private in every context.

const apiKey = process.env.PAYMENT_API_KEY;

await fetch("https://api.example.com/payment", {
  headers: {
    Authorization: `Bearer ${apiKey}`,
  },
});

The example is appropriate when the code executes on a trusted server. If the same secret is bundled into browser-side JavaScript, the secret is no longer secret. Frontend applications should only expose values that are intentionally public.

⚠️ Never assume that an environment variable is private simply because it came from a .env file. What matters is where the value is used and whether the framework exposes it to client-side code.

7. Understand Public Environment Variables

Frameworks such as Next.js provide conventions for intentionally exposing selected environment variables to browser code. In Next.js, variables prefixed with NEXT_PUBLIC_ are intended to be available to the client bundle.

NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=example-id

DATABASE_URL=postgresql://...
PAYMENT_SECRET_KEY=...

A public API URL or analytics identifier may be appropriate for NEXT_PUBLIC_. A database password, private API key or payment secret is not.

💡 Before adding a variable to a public prefix, ask whether you would be comfortable displaying its value in the browser's developer tools. If not, it should stay server-side.

8. Validate Environment Variables at Startup

Environment variables arrive as strings. The application may require a URL, integer, boolean, enum or non-empty secret. Without validation, configuration errors can appear much later as confusing runtime failures.

const port = Number(process.env.PORT);

if (!Number.isInteger(port) || port <= 0) {
  throw new Error("PORT must be a positive integer");
}

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is required");
}

For larger applications, a schema validation library can centralize these checks and provide clearer startup errors. The important idea is to fail early when required configuration is missing or malformed.

9. Remember That Environment Variables Are Strings

A common dotenv mistake is assuming that values automatically become JavaScript numbers or booleans.

PORT=3000
DEBUG=false
MAX_RETRIES=3
console.log(typeof process.env.PORT);       // "string"
console.log(typeof process.env.DEBUG);      // "string"
console.log(typeof process.env.MAX_RETRIES); // "string"

Convert values explicitly at the application's configuration boundary. For booleans, avoid blindly using Boolean(value), because Boolean("false") is true in JavaScript.

const debug = process.env.DEBUG === "true";
const maxRetries = Number(process.env.MAX_RETRIES);

10. Keep Configuration Parsing in One Place

Reading process.env throughout the application makes configuration harder to validate and test. A cleaner architecture is to load and validate environment variables once, then expose a typed configuration object to the rest of the application.

export const config = {
  port: Number(process.env.PORT ?? 3000),
  databaseUrl: process.env.DATABASE_URL ?? "",
  apiBaseUrl: process.env.API_BASE_URL ?? "",
  isProduction: process.env.NODE_ENV === "production",
};

A centralized configuration layer also gives you a single place to document defaults, validate required variables and convert strings into the types expected by the application.

11. Avoid Silent Defaults for Required Secrets

Defaults are useful for optional configuration, but dangerous for credentials and infrastructure settings that must exist. A missing database URL should normally cause an obvious configuration error rather than silently falling back to an unexpected value.

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
  throw new Error("DATABASE_URL is required");
}

For optional settings, explicit defaults can make sense.

const logLevel = process.env.LOG_LEVEL ?? "info";
const timeoutMs = Number(process.env.API_TIMEOUT_MS ?? 10000);

12. Be Careful With Quotes and Special Characters

.env syntax has its own parsing rules. Values containing spaces, special characters or characters with syntactic meaning may require quoting, depending on the dotenv implementation.

APP_NAME="My Application"
DATABASE_PASSWORD="password with spaces"
MESSAGE='Hello world'

Do not add quotes automatically to every value without understanding how the parser treats them. If a value contains characters that could be interpreted as syntax, test the exact file with the parser used by the project.

13. Keep .env Files Simple

Environment files work best when they contain straightforward configuration rather than becoming a second programming language. Avoid complex logic, duplicated definitions and large blocks of application data.

API_BASE_URL=https://api.example.com
API_TIMEOUT_MS=10000
FEATURE_NEW_DASHBOARD=true

If configuration requires complex objects, arrays or nested structures, consider whether a dedicated configuration file or a configuration service would be more appropriate.

14. Do Not Store Large Data Blobs in .env

Environment variables are intended for configuration values, not large documents, certificates, datasets or application content. Large values make deployment, debugging and secret rotation harder.

Some systems do support multiline environment variables, but support and escaping behavior varies. When a value becomes difficult to read or maintain, consider storing the data in a dedicated secret store, mounted file or managed configuration system.

15. Keep .env.example Free of Real Secrets

A common mistake is copying a production .env file into .env.example and forgetting to remove the credentials. The example file should document the required structure without containing working secrets.

DATABASE_URL=
PAYMENT_API_KEY=
AUTH_SECRET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=

For values that have safe development examples, clearly use placeholder values rather than credentials that work against real infrastructure.

16. Use Separate Credentials for Each Environment

Development and production should not normally share the same database password, API key or service account. If a local machine is compromised, shared credentials can turn a local incident into a production incident.

Use separate credentials where the external service supports them. Development credentials can usually have narrower permissions and can be revoked without affecting production.

💡 Treat development, staging and production as separate security boundaries. Reusing production credentials locally increases the impact of accidental leaks.

17. Apply the Principle of Least Privilege

An environment variable can contain a credential that has far more permissions than the application actually needs. Prefer service accounts and API keys with the smallest practical permission set.

For example, an application that only needs to read objects from storage should not necessarily use credentials that can delete every object in the account.

Least privilege limits the consequences of accidental exposure and reduces the blast radius of compromised credentials.

18. Do Not Print Secrets in Logs

Logging the entire process.env object is especially dangerous because it can expose credentials into local logs, CI output, monitoring systems or third-party logging platforms.

// Avoid this
console.log(process.env);

// Prefer logging only safe configuration
console.log({
  nodeEnv: process.env.NODE_ENV,
  port: process.env.PORT,
});
⚠️ Be careful when logging configuration objects. A configuration object that contains a database URL, token or password can expose credentials even if the original .env file remains private.

19. Mask Sensitive Values When Debugging

Sometimes you need to confirm that a secret exists without displaying the secret itself. Logging whether a value is present is safer than printing its contents.

const hasApiKey = Boolean(process.env.API_KEY);

console.log({
  apiKeyConfigured: hasApiKey,
});

If a value must be partially displayed for debugging, expose only a carefully limited representation and make sure the remaining portion cannot be used as a credential.

20. Use Secret Managers in Production When Appropriate

A .env file is convenient for local development, but production systems often provide dedicated secret-management mechanisms. Depending on the infrastructure, secrets may be supplied by a cloud secret manager, CI/CD platform, hosting provider or container orchestration system.

The application can still consume the resulting values through environment variables. The difference is that the deployment system manages the secret instead of requiring a physical .env file to be copied onto the server.

For small applications, hosting-provider environment variables may be sufficient. Larger systems may benefit from dedicated secret-management systems with access policies, auditing and rotation workflows.

21. Treat Docker Environment Variables Carefully

Docker adds another layer of configuration. An application may read values from a .env file during local development, while production injects variables through the container runtime or deployment platform.

services:
  app:
    environment:
      NODE_ENV: production
      API_BASE_URL: https://api.example.com

Avoid baking secrets into Docker images. An image can be stored in registries, cached, copied or inspected by other systems. Secrets that only need to exist at runtime should be supplied through the deployment environment whenever possible.

22. Do Not Confuse Docker Compose Variables With Application Variables

Docker Compose can use .env files for Compose-level variable substitution, while the application inside a container can receive environment variables through the container environment. These are related but not identical mechanisms.

Understanding which layer consumes a variable prevents confusing situations where a value exists in the Compose environment but never reaches the application process.

23. Handle CI/CD Secrets Through the CI System

CI/CD pipelines frequently need credentials for deployments, package registries, cloud providers and external APIs. These values should normally be stored using the CI platform's secret-management features rather than committed into repository files.

The pipeline can expose the secret to the build or deployment process as an environment variable when required. Access should be limited to the workflows and environments that actually need it.

⚠️ Be cautious with pull-request workflows from untrusted branches. A workflow that exposes sensitive environment variables to arbitrary code can unintentionally leak those credentials.

24. Watch for Secrets in URLs

Some applications put credentials directly into connection URLs, for example a database connection string containing a username and password. This can be convenient, but the complete value must be treated as sensitive.

DATABASE_URL=postgresql://user:[email protected]:5432/app

Never expose such a URL in client-side code, public logs, screenshots or error messages. Be especially careful with monitoring tools that automatically record request URLs and exception details.

25. Avoid Putting Secrets in Query Parameters

Even if a secret originates in an environment variable, putting it into a URL can expose it through browser history, reverse proxies, server logs, analytics systems and referrer information.

// Avoid
const url = `https://api.example.com/data?apiKey=${apiKey}`;

When the API supports it, use an appropriate authorization header or another mechanism designed for credentials.

26. Rotate Secrets After Exposure

If a private key, password or access token is accidentally committed, posted in a public issue or exposed in logs, deleting the visible copy is not enough. The credential should be considered compromised.

  • Revoke or rotate the exposed credential.
  • Check where the credential was used and what permissions it had.
  • Remove the secret from the affected repository or logs where appropriate.
  • Inspect Git history if the secret was committed.
  • Check for suspicious activity when the credential provides meaningful access.
  • Update the application with the replacement credential.

The exact response depends on the credential and service, but rotation should happen as quickly as practical when an active secret is exposed.

27. Use Secret Scanning

Automated secret scanning can detect patterns that resemble API keys, tokens and other credentials before they reach production or a public repository. It is useful as an additional safety layer, not as a replacement for careful configuration management.

Scanning can be performed in the repository, pre-commit workflow, CI pipeline or hosting platform. Different scanners support different credential formats, so no scanner should be treated as a guarantee that every secret will be detected.

28. Keep dotenv Loading Explicit

For a small Node.js application, dotenv can be loaded explicitly at the application's entry point.

import "dotenv/config";

import { startServer } from "./server";

startServer();

Another common approach is calling dotenv.config() directly.

import dotenv from "dotenv";

dotenv.config();

console.log(process.env.API_BASE_URL);

Choose one approach and make the initialization order clear. Configuration should be available before modules that depend on it are initialized.

29. Do Not Load dotenv Everywhere

Calling dotenv.config() in many unrelated modules makes configuration initialization harder to reason about. It can also hide dependencies between modules.

A cleaner pattern is to initialize configuration at the application boundary and keep environment parsing inside a dedicated configuration module.

30. Avoid Duplicating the Same Variable Across Files

When the same variable is defined in several .env files, the final value depends on the loading and precedence rules of the framework or dotenv configuration. This can make debugging surprisingly difficult.

# .env
API_URL=https://api.example.com
# .env.local
API_URL=https://local-api.example.com

Duplicated values can be intentional when overriding configuration, but accidental duplication should be avoided. Keep overrides obvious and document which file is expected to take precedence.

31. Use Tools to Inspect and Clean .env Files

As projects grow, environment files can accumulate duplicate variables, inconsistent formatting, unused entries and accidental whitespace. Dedicated dotenv utilities can make these files easier to inspect before committing or deploying them.

A dotenv parser is useful for checking how a file is interpreted. A dotenv cleaner can help identify or remove unnecessary entries, while dotenv merger tools can combine configuration sources when a workflow requires it. Formatters can normalize the presentation of environment files without changing their intended values.

32. Remove Unused Environment Variables

Unused environment variables increase configuration complexity and make it harder to determine which values are actually important. They can also leave old credentials or obsolete service URLs in developer machines and deployment systems.

Periodically search the codebase for environment variable usage and remove variables that are no longer required. This is especially useful after replacing APIs, databases, analytics providers or authentication systems.

33. Document What Each Variable Does

A variable name alone may not explain whether a value is optional, public, required, environment-specific or expected to have a particular format. Documentation can live in .env.example, project documentation or a configuration schema.

# Public API base URL used by the frontend
NEXT_PUBLIC_API_URL=https://api.example.com

# Server-only payment provider key
PAYMENT_SECRET_KEY=

# Optional application log level
LOG_LEVEL=info

Comments are particularly useful when the variable's security boundary is not obvious from its name.

34. Keep Environment Configuration Small and Predictable

A healthy configuration system should be easy to understand when a new developer joins the project. Someone should be able to identify required variables, optional variables, public values and secrets without inspecting the entire application.

  • Use consistent names.
  • Group related variables.
  • Document required values.
  • Keep secrets outside version control.
  • Validate configuration at startup.
  • Remove obsolete variables.
  • Use separate credentials by environment.
  • Avoid exposing server-only values to clients.

35. dotenv Security Checklist

Before deploying an application that uses dotenv or .env files, check the following:

  • Real .env files are excluded from Git.
  • .env.example contains placeholders rather than working secrets.
  • Production credentials are different from development credentials.
  • Required variables are validated at startup.
  • Environment variables are treated as strings and converted explicitly.
  • Sensitive values are never printed to logs.
  • Server-only secrets are not exposed to browser code.
  • Public environment-variable prefixes contain only intentionally public values.
  • Secrets are supplied through a suitable deployment or secret-management system.
  • Docker images do not contain production secrets.
  • CI/CD secrets are stored in the CI platform rather than repository files.
  • Exposed credentials can be revoked and rotated quickly.
  • Unused variables are periodically removed.

Common dotenv Mistakes

MistakeWhy It Is a ProblemBetter Approach
Committing .envSecrets can become part of repository history.Ignore real environment files and rotate exposed credentials.
No .env.exampleDevelopers do not know which variables are required.Document configuration with placeholders.
Using production keys locallyA local compromise can affect production.Use separate development credentials.
Logging process.envPasswords and tokens can enter logs.Log only safe, non-sensitive configuration.
Treating strings as booleansThe value "false" is still a non-empty string.Parse booleans explicitly.
Exposing private keys to the browserClient-side values can be inspected by users.Keep secrets on the server.
Duplicating variables across filesPrecedence becomes difficult to understand.Use deliberate and documented overrides.
Keeping obsolete variablesConfiguration becomes harder to audit.Remove unused variables periodically.

A Practical dotenv Workflow

A simple workflow can keep environment configuration manageable from development through production.

  • Create a .env.example containing all required configuration names.
  • Create a local environment file with machine-specific values.
  • Add private environment files to .gitignore.
  • Load configuration at the application boundary.
  • Validate required variables during startup.
  • Convert strings into the required application types.
  • Keep server-only credentials on the server.
  • Store production secrets in the deployment platform or secret manager.
  • Use separate credentials for development, staging and production.
  • Rotate credentials when exposure is suspected.

This workflow is simple enough for a small application but also scales well as the project moves toward automated deployments and multiple environments.

dotenv vs a Secret Manager

dotenv and secret managers solve different problems. dotenv is primarily a convenient way to load configuration into a local or server process. A secret manager is designed to control access to sensitive values and often provides features such as access policies, auditing and rotation.

Featuredotenv / .envSecret Manager
Local developmentVery convenientUsually requires additional setup
Encryption at restNot provided by dotenv itselfUsually supported by the service
Access controlMostly filesystem or deployment permissionsDedicated access policies
Secret rotationManual unless automated separatelyOften supported or easier to automate
Audit trailNot providedOften available
Simple projectsEasy to useMay be unnecessary complexity
Large production systemsUsually not sufficient by itselfOften more appropriate

The two approaches can also coexist. Local development can use .env files while production retrieves secrets from a managed system and exposes them to the application through environment variables.

dotenv Best Practices for Next.js

Next.js has its own environment-variable loading behavior, so applications should follow Next.js documentation rather than assuming that generic dotenv precedence rules apply everywhere.

A practical Next.js setup is to keep server-only credentials in ordinary environment variables and use NEXT_PUBLIC_ only for values that are intentionally exposed to the browser.

# Public
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_ANALYTICS_ID=example

# Server-only
DATABASE_URL=postgresql://...
AUTH_SECRET=...
PAYMENT_SECRET_KEY=...

Server-side code can safely use private variables when the values remain on the server. Client components should not be given credentials simply because the application can technically read them during development.

dotenv Best Practices for Node.js

For a standalone Node.js application, dotenv can be initialized before the modules that depend on environment variables.

import "dotenv/config";

const port = Number(process.env.PORT ?? 3000);

if (!Number.isInteger(port) || port <= 0) {
  throw new Error("Invalid PORT");
}

console.log(`Starting server on port ${port}`);

For larger projects, move validation and conversion into a dedicated configuration module rather than repeating process.env access throughout the codebase.

Frequently Asked Questions

Should .env files be committed to Git?

Real .env files containing environment-specific configuration or secrets should normally not be committed. Use .env.example or another documented template to describe required variables without exposing credentials.

Is dotenv secure?

dotenv is a configuration-loading library, not a secret-management system. It does not encrypt your .env file, rotate credentials or prevent secrets from being exposed through Git, logs or client-side code.

Should I use .env or .env.local?

That depends on the framework and project setup. The important principle is to separate shared configuration from machine-specific overrides and to follow the framework's documented file precedence.

Are environment variables always private?

No. An environment variable can become public when a framework exposes it to client-side code or when application code sends the value to the browser. Treat variables as private only when the complete data flow keeps them on the trusted server side.

Should environment variables be validated?

Yes. Required variables, URLs, numbers, booleans and allowed values should be validated when the application starts. Early validation makes configuration errors easier to diagnose.

Can I use dotenv in production?

Yes, but a physical .env file is not always the best production configuration mechanism. Hosting platforms, CI/CD systems and secret managers can provide environment variables directly to the application without storing a production .env file on disk.

What should I do if a secret was committed to Git?

Treat the credential as exposed. Revoke or rotate it, inspect its permissions and investigate where it may have been accessed. Removing the file from the latest commit does not invalidate a credential or erase it from repository history.

Helpful dotenv Tools

When working with environment configuration, a dotenv parser can help inspect how variables are interpreted, while a dotenv cleaner can help remove unnecessary or problematic entries. A dotenv merger is useful when several configuration sources need to be combined, and environment-variable or ENV formatters can normalize files into a consistent structure.

These tools are especially useful when reviewing configuration before committing, migrating between projects or cleaning up a large collection of environment variables.

Conclusion

Good dotenv practices are mostly about treating configuration as a deliberate part of the application's architecture rather than as a collection of random key-value pairs. Keep real secrets out of Git, document required variables with an example file, use consistent names, separate environments, validate configuration and keep server-only values away from client-side code.

For local development, .env files remain a simple and practical solution. As an application grows, production secrets can move into hosting-provider variables, CI/CD secrets or dedicated secret-management systems while the application continues to consume them through the environment.

The most important goal is predictable configuration: developers should know which variables are required, deployments should fail clearly when configuration is invalid, and sensitive credentials should have a controlled lifecycle from creation through rotation and removal.

Found an issue?

Found an error, outdated information, or something missing from this article? Let me know through the Contact page.

Your feedback helps improve our articles and keep them accurate and useful.