Ctrl + K
Environment21 min read

Working with .env Files

A practical guide to .env files covering environment variable syntax, quotes, comments, multiple environments, dotenv, Next.js, Docker, security and common configuration mistakes.

Published: 2026-10-05

Configuration is a normal part of almost every application. Database URLs, API endpoints, feature flags, credentials and other settings often need to change between development, testing and production. Hard-coding these values directly into source code makes applications harder to configure and can expose sensitive information.

Environment variables provide a standard way to keep configuration outside application code. A .env file is one of the most common ways to define those variables during local development and in tools that support dotenv-style configuration.

This guide explains how .env files work, how their syntax is structured, how to use multiple environment files, how frameworks such as Next.js load them, how dotenv libraries fit into the process, and how to avoid common security and configuration mistakes.

What Is a .env File?

A .env file is a plain-text configuration file containing environment variable assignments. The filename commonly used for the default file is simply .env.

NODE_ENV=development
PORT=3000
API_URL=https://api.example.com
DATABASE_URL=postgresql://localhost:5432/myapp

Each line normally defines a variable name followed by an equals sign and its value. An application, framework or dotenv-compatible loader can read the file and make those values available as environment variables.

The exact behavior depends on the tool loading the file. The .env format is widely used, but there are differences between dotenv implementations, operating systems, shells and frameworks. When behavior matters, always check the parser or framework actually used by the project.

Environment Variables vs .env Files

An environment variable and a .env file are not the same thing. An environment variable is a value exposed to a running process through its environment. A .env file is one possible source from which those values can be loaded.

For example, a process may receive DATABASE_URL directly from a hosting platform. During local development, the same variable may instead be read from .env.

ConceptMeaning
.env fileA text file containing configuration assignments
Environment variableA value available to a running process
dotenvA family of libraries and conventions for loading .env files
Shell environmentVariables provided by the operating system or command shell
Hosting configurationEnvironment variables configured by a deployment platform

Basic .env Syntax

The basic syntax is straightforward: write a variable name, an equals sign and a value.

APP_NAME=My Application
APP_PORT=3000
DEBUG=true
API_URL=https://api.example.com

Variable names commonly use uppercase letters with underscores, although the exact set of accepted names depends on the environment and parser. A consistent naming convention makes configuration easier to understand.

Variable Names

A common convention is to use uppercase names with underscores separating words.

DATABASE_URL=postgresql://localhost:5432/app
API_BASE_URL=https://api.example.com
MAX_RETRIES=3
ENABLE_CACHE=true

Avoid using confusing or inconsistent names for the same concept across different environments. If one project uses API_BASE_URL, changing it to API_URL in another file can create unnecessary configuration errors.

Values Are Usually Strings

One of the most important details about environment variables is that they are generally exposed to applications as strings.

PORT=3000
DEBUG=false
MAX_CONNECTIONS=20

The application does not automatically receive the number 3000 or the boolean false merely because the values look like those types. In JavaScript, for example, process.env.PORT is typically the string "3000" and process.env.DEBUG is the string "false".

const port = Number(process.env.PORT);
const debug = process.env.DEBUG === "true";
const maxConnections = Number(process.env.MAX_CONNECTIONS);
⚠️ Do not treat process.env values as typed data automatically. Convert and validate values explicitly when the application expects numbers, booleans, URLs, enums or other types.

Empty Values

A variable can have an empty value depending on the parser and syntax being used.

OPTIONAL_VALUE=
ANOTHER_VALUE=""

These forms can represent an explicitly empty value. The exact parsing behavior can differ between implementations, so projects should rely on the syntax supported by their chosen dotenv loader.

Quotes in .env Files

Quotes are commonly used when a value contains spaces or characters that could otherwise be interpreted specially.

APP_NAME="My Application"
WELCOME_MESSAGE='Hello, developer!'

Whether quotes are removed, how escape sequences work and how multiline values are handled depends on the parser. Do not assume that every tool implementing .env support has identical behavior.

Comments

Comments are commonly written using a hash character.

# Application settings
APP_PORT=3000

# API configuration
API_URL=https://api.example.com

Comments are useful for explaining configuration sections, documenting required variables and making large environment files easier to maintain.

Inline Comments

Inline comment behavior is parser-dependent. Some dotenv implementations recognize a hash after a value as the beginning of a comment, while values containing a hash may require quoting to make the intended value explicit.

APP_PORT=3000 # Development server
PASSWORD="abc#123"

When a value contains characters that could be interpreted as syntax, quoting it is often clearer and safer than relying on subtle parser behavior.

Whitespace

Whitespace around assignments is another area where implementations can differ. Keep .env files simple and consistently formatted rather than relying on unusual spacing.

API_URL=https://api.example.com
PORT=3000
DEBUG=true

A formatter can help normalize spacing and reduce accidental syntax differences when multiple developers edit the same configuration files.

Multiline Values

Some dotenv implementations support multiline quoted values, which can be useful for certificates, private keys or other structured text. However, multiline support is not something that should be assumed across every environment-variable parser.

PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
example-content
-----END PRIVATE KEY-----"

For sensitive multiline data, consider whether putting the entire value into an environment variable is actually the best design. Secret-management systems or mounted files may be more appropriate for production credentials and certificates.

Variable Expansion

Some dotenv tools support variable expansion, allowing one variable to reference another. For example:

HOST=localhost
PORT=3000
API_URL=http://${HOST}:${PORT}

This feature is not universal across all .env implementations. If a project relies on variable expansion, verify that the actual framework or dotenv library supports it instead of assuming that every .env parser will resolve references.

The .env File Is Usually Not Committed

Local .env files frequently contain secrets such as API keys, database passwords, private credentials or service tokens. For that reason, many projects add .env to .gitignore.

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

The exact files that should be ignored depend on the project's environment strategy. The important principle is to prevent local secret-containing configuration from accidentally entering the repository.

Use .env.example for Documentation

A useful alternative to committing the real .env file is to commit a template containing the required variable names without actual secrets.

NODE_ENV=development
PORT=3000
DATABASE_URL=
API_KEY=
API_BASE_URL=https://api.example.com

A developer can copy the template into a local .env file and fill in the values required for the project.

cp .env.example .env
💡 Treat .env.example as part of the project's configuration documentation. When adding a new required environment variable, update the example file and document what the variable controls.

Different Environment Files

Applications often need different configuration for development, testing and production. Frameworks and dotenv tools may support naming conventions such as .env.development, .env.test and .env.production.

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

The exact precedence and loading behavior is framework-specific. Never assume that a filename automatically has a particular meaning in every Node.js project.

Local Environment Overrides

A common pattern is to keep shared defaults in a tracked environment file and machine-specific overrides in an ignored local file.

.env.development
API_URL=https://dev-api.example.com
LOG_LEVEL=debug
.env.local
API_URL=http://localhost:4000
LOG_LEVEL=debug

The important part is not the filename itself but the loading rules of the framework. For example, Next.js has its own documented environment-file loading order.

How dotenv Works

In a Node.js application, the dotenv package is commonly used to load values from a .env file into process.env.

import "dotenv/config";

console.log(process.env.API_URL);

The important distinction is that dotenv does not create a new environment-variable system. It reads configuration from a file and populates the Node.js process environment according to the library's behavior.

Explicit dotenv Loading

A project can also load dotenv explicitly when more control is required.

import dotenv from "dotenv";

dotenv.config();

const databaseUrl = process.env.DATABASE_URL;

This style can be useful when initialization order matters or when the application needs to configure the dotenv loader explicitly.

Loading a Specific .env File

Some dotenv implementations allow a specific file path to be provided.

import dotenv from "dotenv";

dotenv.config({
  path: ".env.test",
});

console.log(process.env.DATABASE_URL);

This can be useful in scripts, tests or custom application bootstrapping, but framework-managed environment loading should generally be left to the framework when it already provides that functionality.

Working with .env Files in Next.js

Next.js has built-in support for environment variables and several environment-file conventions. This means a typical Next.js application does not need to install dotenv merely to load its standard .env files.

DATABASE_URL=postgresql://localhost:5432/app
API_SECRET=server-only-value
NEXT_PUBLIC_API_URL=https://api.example.com

One important Next.js rule is that variables intended for browser-side code generally need the NEXT_PUBLIC_ prefix. Those values can be exposed to the client bundle, so the prefix should never be used for secrets.

⚠️ NEXT_PUBLIC_ does not mean secure or private. Treat every variable exposed with that prefix as public data because client-side JavaScript can access it.

Server-Only Environment Variables

Database credentials, private API keys and signing secrets should remain on the server. In a Next.js application, keep them in server-side code and do not pass them into client components or serialize them into browser-visible data.

const apiKey = process.env.API_SECRET;

if (!apiKey) {
  throw new Error("API_SECRET is not configured");
}

The exact server/client boundary depends on the framework and application architecture, but the security principle is universal: a secret stops being secret once it reaches the browser.

Environment Variables in Docker

Docker applications can receive environment variables from several sources, including Docker Compose configuration, command-line options, environment files and the deployment environment.

services:
  app:
    image: example-app
    env_file:
      - .env

Using an env_file can be convenient for local development, but production secret management should be considered separately. A container image should not contain private credentials merely because the application needs them at runtime.

.env Files and Docker Images

A common mistake is copying .env into a Docker image during the build process. If the file contains secrets, those credentials may become part of an image layer or otherwise become accessible to people who can inspect the image.

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "start"]

If .env should not be included in the build context, add it to .dockerignore and provide runtime configuration through the deployment environment or an appropriate secret mechanism.

.env
.env.*
node_modules

Never Put Secrets Directly in Git

A .env file is not inherently secure. It is simply a convenient text representation of configuration. If a secret is committed to Git, deleting the file in a later commit does not necessarily remove it from repository history.

If a real secret is accidentally exposed, removing the file is not enough. The credential should generally be revoked or rotated, and the repository history should be handled according to the organization's incident-response process.

⚠️ If an API key, database password or private token has been committed or publicly exposed, assume that the credential may have been compromised. Rotate it instead of relying only on deleting the file.

Do Not Use .env Files as a Database

Environment variables work well for relatively small pieces of configuration. They are not a replacement for a configuration database or application data store.

Large collections of dynamic values, user-specific settings, frequently changing business data and structured application state generally belong somewhere else. An environment variable should normally represent configuration required by the application rather than arbitrary application data.

Validating Environment Variables

Loading an environment file successfully does not mean that the application configuration is correct. A required variable can still be missing, malformed or assigned an unexpected value.

const databaseUrl = process.env.DATABASE_URL;

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

For larger applications, schema-based validation can provide stronger guarantees. A validation library can check required values, convert types and reject invalid configuration before the application starts serving requests.

Validate URLs Explicitly

const apiUrl = process.env.API_URL;

if (!apiUrl) {
  throw new Error("API_URL is required");
}

const parsedUrl = new URL(apiUrl);

This is safer than assuming that every non-empty string is a valid URL.

Validate Numbers and Booleans

const port = Number(process.env.PORT);
const debug = process.env.DEBUG === "true";

if (!Number.isInteger(port) || port < 1 || port > 65535) {
  throw new Error("PORT must be a valid port number");
}

Explicit conversion prevents subtle bugs caused by comparing strings with numbers or assuming that values such as false, 0 or an empty string have the expected semantics.

Environment Variable Naming Conventions

A consistent naming convention becomes increasingly important as a project grows. Names should communicate the purpose of the variable without requiring developers to inspect the code that consumes it.

PatternExample
Database connectionDATABASE_URL
External API endpointPAYMENT_API_URL
Private credentialPAYMENT_API_KEY
Feature flagENABLE_NEW_CHECKOUT
Numeric settingMAX_RETRIES
Public client variableNEXT_PUBLIC_API_URL

Avoid Ambiguous Names

Names such as KEY, URL or SECRET can become difficult to understand when an application grows. More specific names make configuration easier to maintain.

STRIPE_SECRET_KEY=
PAYMENT_API_BASE_URL=
DATABASE_URL=
SESSION_SECRET=

Separate Configuration from Secrets

Not every environment variable contains sensitive information. PORT, NODE_ENV, LOG_LEVEL and feature flags may be ordinary configuration, while API keys, database passwords and signing secrets require stronger protection.

Keeping this distinction in mind helps teams decide which values can be safely documented, which can be committed as defaults and which must be injected through a secret-management mechanism.

Production .env Files

A local .env file is convenient during development, but production environments do not necessarily need a physical .env file. Many hosting platforms provide environment-variable configuration directly through their dashboards, deployment configuration or secret-management systems.

This can reduce the risk of copying a production secret file between machines and makes deployment configuration easier to control separately from application source code.

Development vs Production Configuration

DevelopmentProduction
Local .env files are commonPlatform-managed variables are common
Local database may be usedManaged database or production cluster
Debug logging may be enabledLogging is usually more controlled
Test credentials may be acceptableProduction credentials must be protected
Developers edit configuration locallyAccess should be controlled

Common .env Mistakes

  • Committing .env files containing real credentials.
  • Assuming every environment loader supports exactly the same syntax.
  • Treating all environment variables as numbers or booleans without conversion.
  • Exposing server secrets to browser-side code.
  • Using NEXT_PUBLIC_ for values that should remain private.
  • Copying secrets into Docker images.
  • Forgetting to update .env.example after adding a required variable.
  • Hard-coding production credentials in source code.
  • Using different variable names for the same setting across environments.
  • Failing to validate required configuration during application startup.
  • Relying on .env files as a substitute for proper secret management.
  • Assuming that deleting a leaked secret from Git automatically makes it safe.

Debugging .env Problems

When an environment variable appears to be missing, start by checking whether the file is actually being loaded. Then verify the filename, location, variable name, loading order and process that consumes the value.

It is also important to distinguish between the shell environment and the .env file. A variable already exported by the shell or hosting platform may take precedence over a value from a file depending on the loader.

console.log({
  nodeEnv: process.env.NODE_ENV,
  apiUrl: process.env.API_URL,
  hasApiKey: Boolean(process.env.API_KEY),
});

When debugging secrets, log whether a value exists rather than printing the actual secret.

Check the Current Process Environment

In Node.js, process.env exposes the environment variables available to the current process. If a value is missing, inspect the application's startup and loading configuration before changing the application logic.

Avoid commands or debug output that dump the complete environment to logs. CI systems and hosting platforms may inject sensitive credentials into the process environment.

Using .env Files in CI/CD

Continuous integration systems often provide environment variables through encrypted secrets or protected configuration rather than requiring a committed .env file.

A CI pipeline might use the same variable names as local development while sourcing the values from the CI platform. This allows application code to remain consistent while credentials and environment-specific values stay outside the repository.

Do Not Print Environment Variables in CI Logs

Debugging CI configuration can be tempting, but printing the entire environment can expose credentials. Prefer checking individual non-sensitive values or whether a required secret exists.

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

console.log("Database configuration is available");

Managing Multiple .env Files

Multiple environment files can be useful when an application has separate development, testing and production configurations. However, too many overlapping files can make precedence difficult to understand.

Keep the environment strategy simple. Document which files are used, which variables are required and which file takes precedence. When a framework provides an established loading order, follow that order rather than creating an independent configuration system.

Merging .env Files

Some workflows need to combine configuration from multiple environment files. For example, a shared base file might contain defaults while an environment-specific file overrides selected values.

# .env
APP_NAME=Example
LOG_LEVEL=info
API_URL=https://api.example.com

# .env.local
LOG_LEVEL=debug

A merger tool can be useful for inspecting or combining these files, but the final precedence rules should remain explicit. Never merge files blindly when they contain secrets or environment-specific credentials.

Splitting Large .env Files

A very large environment file can become difficult to review. Splitting configuration into logical files may improve maintainability, provided the application's loading strategy remains clear.

For example, local development may separate general application settings from service-specific configuration. The decision should be based on the project's tooling and deployment model rather than a desire to create as many files as possible.

Formatting .env Files Consistently

A consistent format makes configuration easier to review and reduces accidental syntax errors. Group related variables together, use descriptive names and avoid unnecessary quoting or unusual syntax.

# Application
NODE_ENV=development
PORT=3000

# Database
DATABASE_URL=postgresql://localhost:5432/app

# API
API_BASE_URL=https://api.example.com
API_TIMEOUT=10000

# Features
ENABLE_CACHE=true

Security Checklist for .env Files

  • Keep real secrets out of Git repositories.
  • Add appropriate .env files to .gitignore.
  • Commit a safe .env.example when the project needs a configuration template.
  • Never expose server-only secrets to browser code.
  • Do not put secrets into NEXT_PUBLIC_ variables.
  • Avoid copying secrets into Docker images.
  • Use managed secrets or protected environment configuration in production.
  • Rotate credentials if they are accidentally exposed.
  • Avoid printing secret values in application or CI logs.
  • Restrict access to production configuration.

A Practical .env Workflow

Start by defining the configuration variables the application actually needs. Give them consistent names and document their purpose. Create a safe .env.example containing variable names and non-sensitive defaults where appropriate.

During local development, create the appropriate .env file and keep it outside version control when it contains private values. Let the framework or dotenv loader read the file during application startup.

Validate important variables before the application starts. Convert strings to the required types and reject missing or malformed configuration early instead of allowing configuration errors to appear later during requests.

For production, inject configuration through the deployment environment or an appropriate secret-management system. Keep the same variable names across environments whenever possible so that application code does not need environment-specific branches just to find its configuration.

Example Project Configuration

.env.example
.env
.env.local
.env.development
.env.production

A simple project might use .env.example as the documented template, .env for shared local defaults, .env.local for machine-specific overrides and environment-specific files when the framework supports them.

The exact combination should be kept as small as practical. The more overlapping files a project introduces, the more important it becomes to document loading and precedence rules.

Working with .env Files Safely

The main security principle is simple: a .env file should be treated as configuration, not as a secure vault. Anyone who can read a file containing an API key or database password can potentially use that credential.

For local development, .env files are convenient and practical. For production systems, use the configuration and secret-management facilities provided by the hosting platform or infrastructure whenever possible.

💡 Keep the application's variable names stable across environments while changing the values through deployment configuration. This makes the application easier to deploy and reduces environment-specific code.

Frequently Asked Questions

What is a .env file used for?

A .env file is commonly used to define environment variables for local development and other dotenv-compatible workflows. It can contain configuration such as API URLs, ports, database connection strings and feature flags.

Should .env files be committed to Git?

Real .env files containing secrets should generally not be committed to a Git repository. A safer pattern is to commit a .env.example file containing variable names and non-sensitive example values.

Are values in a .env file strings?

Environment variables are generally exposed to applications as strings. Values that represent numbers, booleans, URLs or other types should be explicitly parsed and validated by the application.

What is the difference between .env and .env.example?

.env usually contains the actual local configuration and may contain secrets, while .env.example is a safe template that documents which variables the project expects without containing real credentials.

Do I need dotenv in Next.js?

Next.js provides built-in support for its documented environment-file conventions, so a typical Next.js application does not need to install dotenv merely to load standard .env files.

Is a .env file secure?

A .env file is only a plain-text configuration file and should not be treated as a secure vault. Protect the file from unauthorized access, keep secrets out of source control and use appropriate secret-management facilities for production.

Can I use multiple .env files?

Yes. Many frameworks and dotenv-compatible tools support multiple environment files for different environments or local overrides. The exact filenames, precedence and loading order depend on the framework or parser.

Helpful Environment Tools

A dotenv parser is useful for inspecting and understanding variables stored in an environment file, while a dotenv merger can help combine configuration from multiple files when the workflow requires it. A dotenv splitter can separate a large configuration file into smaller groups, and an Environment Variable Formatter can normalize the formatting of environment assignments. An ENV File Validator is useful for detecting malformed entries and configuration problems before the file is consumed by an application.

Conclusion

.env files provide a simple and convenient way to manage environment-specific configuration, especially during local development. Their basic syntax is easy to understand, but details such as quoting, variable expansion, multiline values and file precedence can vary between dotenv implementations and frameworks.

The most important practices are to keep secrets out of source control, use a safe .env.example template, validate configuration at startup, convert environment variables to the types the application expects and keep server-only values away from browser code.

For production applications, .env files are only one possible configuration mechanism. Hosting platforms, CI/CD systems, containers and secret-management services can provide environment variables without requiring sensitive files to be stored alongside application source code.

Used with a clear naming convention and a documented loading strategy, environment variables provide a flexible boundary between application code and environment-specific configuration.

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.