Ctrl + K
Environment21 min read

Managing Secrets in .env Files

A practical guide to managing secrets in .env files, covering API keys, passwords, tokens, Git security, encryption, secret rotation, CI/CD, Docker, production deployments and common mistakes.

Published: 2026-10-05

Environment variables are one of the most common ways to provide applications with configuration and credentials. Database passwords, API keys, access tokens, encryption keys and other sensitive values are often loaded through environment variables, especially during local development.

The convenience of .env files also creates a security risk: they are ordinary text files. Anyone who can read the file can potentially read the secrets stored inside it. A .env file is therefore a configuration mechanism, not a secure vault.

Safe secret management requires more than adding .env to .gitignore. Developers need to understand how secrets enter a project, where they are stored, how they reach the application, how they can accidentally leak and what to do when a secret is exposed.

What Counts as a Secret?

A secret is information that should be available only to authorized applications or people. In software projects, secrets commonly include credentials, private keys and authentication material.

Secret typeExamples
API keyThird-party service API credentials
Access tokenOAuth or service access token
Database credentialDatabase username and password
Signing secretSecret used to sign sessions or tokens
Encryption keyKey used to encrypt or decrypt data
Private keyTLS, SSH or application cryptographic private key
Webhook secretSecret used to verify webhook requests
Service credentialCredential used to authenticate with another service

Not every environment variable is a secret. Values such as PORT=3000, NODE_ENV=production or LOG_LEVEL=info may be ordinary configuration. The distinction matters because secrets require stronger protection and access controls.

Why Secrets Are Stored in Environment Variables

Hard-coding credentials directly into application source code makes configuration difficult to change and creates an obvious path for accidental disclosure. Environment variables provide a separate configuration channel.

const apiKey = process.env.PAYMENT_API_KEY;

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

The application source code does not need to contain the actual key. A development machine, CI system or hosting platform can provide the value when the application starts.

💡 The goal is not to make a secret invisible. The goal is to keep the secret out of places where it does not need to exist, especially source code, browser bundles, logs and public repositories.

A .env File Is Not a Secret Store

A .env file is usually plain text. If it contains a value such as an API key, opening the file with a text editor is enough to reveal the key.

DATABASE_URL=postgresql://user:[email protected]:5432/app
PAYMENT_API_KEY=secret-api-key
SESSION_SECRET=another-secret

The file may be appropriate for local development, but its security depends on the operating system, file permissions, repository configuration and every place where the file is copied.

⚠️ Do not assume that a file is secure simply because its name starts with a dot. .env is hidden by convention on some systems, but hidden does not mean encrypted or protected.

Never Commit Real Secrets to Git

One of the most important rules of secret management is to keep real credentials out of source control. A common first step is adding local environment files to .gitignore.

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

This prevents untracked matching files from being added accidentally, but .gitignore does not remove a file that has already been committed. It also does not protect secrets that were copied into another file, pasted into source code or exposed through logs.

What If a Secret Was Already Committed?

If a real credential has been committed to a repository, deleting the line in a later commit does not make the credential safe again. The secret may still exist in previous commits, clones, forks, caches or other copies of the repository.

The first priority is usually to revoke or rotate the exposed credential. Repository cleanup can then be handled separately using the organization's procedures and appropriate history-rewriting tools when necessary.

⚠️ Treat an exposed API key, password or token as compromised. Do not rely on removing the file from the latest commit. Rotate or revoke the credential as soon as possible.

Secret Rotation

Secret rotation means replacing an existing credential with a new one. Rotation limits the lifetime of a credential and provides a recovery mechanism when a secret is exposed or needs to be replaced.

A basic rotation process is to create a new credential, update the applications that depend on it, verify that the new credential works and then revoke the old credential when it is no longer required.

  • Generate or obtain a new credential.
  • Update the protected environment configuration.
  • Deploy or restart the affected application.
  • Verify that requests using the new credential succeed.
  • Revoke the old credential.
  • Check logs and monitoring for authentication failures.

Avoid Hard-Coding Secrets

Hard-coding a credential in source code creates several problems. The secret becomes part of the repository, can appear in code review, can be copied into builds and may remain in version history even after the code is changed.

// Avoid
const apiKey = "sk-example-secret";

// Prefer
const apiKey = process.env.API_KEY;

The second approach does not make the value automatically secure, but it keeps the application code independent from a specific credential.

Use .env.example Without Real Secrets

A project usually needs to tell developers which environment variables are required. The safest way is to commit a template containing variable names and non-sensitive example values.

DATABASE_URL=
PAYMENT_API_KEY=
SESSION_SECRET=
API_BASE_URL=https://api.example.com

The actual .env file remains local or is provided by the development environment. This gives developers a documented configuration contract without putting real credentials in the repository.

Use Placeholders Carefully

A placeholder should be obviously non-functional. Do not put a real API key into .env.example merely because it is convenient for development.

PAYMENT_API_KEY=your-api-key-here
DATABASE_PASSWORD=replace-me
SESSION_SECRET=generate-a-local-secret

For local services, a clearly documented development-only credential may be acceptable when it has no access to production resources. Production credentials should never be reused as examples.

Use Separate Credentials for Each Environment

Development, testing and production should not normally share the same credentials. Separating credentials limits the consequences of a development machine being compromised or a test environment being accidentally exposed.

EnvironmentRecommended approach
DevelopmentLocal or development-only credentials
TestingDedicated test credentials and services
StagingSeparate staging credentials
ProductionProtected production credentials

The exact setup depends on the project, but production credentials should be treated as a separate security boundary rather than copied into developer machines.

Do Not Reuse Production Secrets Locally

Using production API keys or database passwords on a developer computer increases the number of places where highly sensitive credentials exist. It also makes it easier for local debugging tools, terminal history, backups or accidental uploads to expose production access.

Whenever possible, create credentials with only the permissions and environment required for development.

Principle of Least Privilege

A secret should provide only the permissions required for the application or task that uses it. If a service supports separate credentials with different permissions, use the narrowest scope that satisfies the application's needs.

For example, a read-only reporting process should not need a credential capable of deleting production data. Limiting permissions reduces the potential impact if the credential is exposed.

Secret Permissions Matter

A secret is only as protected as the systems that can access it. If every developer, CI job and production service receives every secret, the security boundary becomes unnecessarily large.

Separate credentials by application, environment and purpose when practical. This also makes rotation easier because changing one credential does not necessarily require updating unrelated systems.

Encrypting .env Files

Some teams encrypt environment files so that encrypted configuration can be stored or transferred more safely. An encrypted .env file is fundamentally different from an ordinary .env file because the sensitive values are not directly readable without the decryption key.

Encryption can be useful when a project needs to distribute encrypted configuration through a controlled workflow. However, encryption does not eliminate the need to protect the decryption key.

Encrypted configuration
+
Decryption key
+
Trusted runtime
=
Application configuration

The decryption key must be protected separately from the encrypted data. If both the encrypted file and its decryption key are stored together without meaningful access controls, encryption provides little practical protection.

Encryption Is Not the Same as Encoding

Encoding transforms data into another representation without providing confidentiality. Base64 is a common example. Anyone who has the encoded value can decode it.

Secret:
my-api-key

Base64:
bXktYXBpLWtleQ==
⚠️ Never treat Base64, hexadecimal or similar encoding as encryption. Encoded secrets are still secrets and should be protected exactly as the original values.

Where Should the Encryption Key Live?

The key used to decrypt an encrypted configuration file must come from a protected source. Storing the encrypted file and its decryption key in the same repository defeats much of the purpose of encryption.

Possible sources include a deployment platform's secret configuration, a dedicated secret-management service or another protected runtime mechanism. The appropriate choice depends on the infrastructure and threat model.

Secret Generators

Secrets should generally be generated using a cryptographically secure random source rather than manually invented strings. This is particularly important for session secrets, encryption keys, reset tokens and other values where unpredictability matters.

import { randomBytes } from "node:crypto";

const secret = randomBytes(32).toString("hex");

console.log(secret);

The required length and representation depend on how the secret will be used. A cryptographic key has different requirements from a session secret or an API credential issued by an external service.

Do Not Generate Secrets with Math.random()

JavaScript's Math.random() is not intended for generating security-sensitive secrets. Use a cryptographically secure random number generator provided by the runtime or a trusted cryptographic library.

// Not appropriate for security-sensitive secrets
const weakSecret = Math.random().toString(36);

// Use a cryptographically secure generator instead
import { randomBytes } from "node:crypto";

const secureSecret = randomBytes(32).toString("hex");

Mask Secrets in Logs

Logging configuration is useful when debugging an application, but logging secret values can turn an otherwise protected credential into a leaked credential.

const apiKey = process.env.API_KEY;

console.log({
  apiKeyConfigured: Boolean(apiKey),
});

When it is necessary to display a secret-like value for debugging, masking can reduce accidental exposure. However, masking is not a replacement for keeping the secret out of logs in the first place.

Secret Masking

A masking function can replace most characters with a placeholder while retaining a small amount of information for identification.

function maskSecret(value: string, visible = 4) {
  if (value.length <= visible) {
    return "*".repeat(value.length);
  }

  return "*".repeat(value.length - visible) + value.slice(-visible);
}

console.log(maskSecret("example-secret-value"));

Even masked values should be handled carefully. If a secret is extremely short or has a recognizable structure, revealing its last characters may still provide information that should not be exposed.

Do Not Log process.env

Printing the entire process environment is a common debugging shortcut that can expose database credentials, API keys, tokens and deployment secrets.

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

// Prefer
console.log({
  nodeEnv: process.env.NODE_ENV,
  databaseConfigured: Boolean(process.env.DATABASE_URL),
  apiConfigured: Boolean(process.env.API_KEY),
});

Browser Exposure Is a Major Risk

A server-side environment variable can be private, but once its value is sent to browser JavaScript it is no longer a secret. Client-side applications can be inspected by users through browser developer tools, downloaded JavaScript bundles or network requests.

// Server-side secret
const privateKey = process.env.PAYMENT_SECRET_KEY;

// Never send the private key to the browser.
⚠️ Never assume that an environment variable remains private merely because its name came from a .env file. Privacy depends on where the value is used and whether it crosses the server-client boundary.

Next.js and Secret Variables

Next.js uses a NEXT_PUBLIC_ prefix for environment variables intended to be available to browser-side code. This makes the prefix an important security boundary in a Next.js application.

# Server-only
DATABASE_URL=postgresql://localhost:5432/app
PAYMENT_SECRET_KEY=secret-value

# Public
NEXT_PUBLIC_API_URL=https://api.example.com

Only values that are genuinely safe to expose should use the NEXT_PUBLIC_ prefix. A payment secret, database password, signing secret or private API credential should never be placed there.

Secrets in Docker

Containers add another layer to secret management. Developers often use .env files with Docker Compose during local development, but production containers should not automatically receive every local environment file.

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

This can be convenient locally. For production, use the secret and environment-variable facilities provided by the deployment platform or container orchestration system when available.

Do Not Copy Secrets into Docker Images

Copying a .env file into an image can make secrets part of the image's filesystem or build history. Anyone with sufficient access to the image may then be able to inspect those values.

.env
.env.*
node_modules

A safer pattern is to build an image without private configuration and provide secrets when the container starts.

Secrets in CI/CD

CI/CD systems commonly provide protected variables or secret stores that can be injected into build and deployment jobs. This avoids committing production credentials into repositories.

A pipeline might expose variables such as DATABASE_URL or DEPLOY_TOKEN only to the specific job that needs them. Restricting which workflows can access a secret reduces unnecessary exposure.

⚠️ Be careful with CI logs. A command that prints an environment variable can expose a secret even when the CI platform stores that secret securely.

Pull Requests and Code Reviews

Secrets can leak without ever being committed intentionally. Developers may paste a .env fragment into a pull request, issue, chat message or screenshot while asking for help with a configuration problem.

Before sharing configuration publicly or with people who do not need access, replace real credentials with clearly fake placeholders.

DATABASE_URL=postgresql://user:REDACTED@host:5432/database
API_KEY=REDACTED
SESSION_SECRET=REDACTED

Secret Scanning

Secret scanning tools inspect source code and repository changes for patterns that resemble credentials. They can catch some accidental leaks before they become a larger incident.

Secret scanning is useful as a second line of defense, but it should not replace good repository hygiene. A scanner can miss credentials that do not match known patterns, while false positives are also possible.

Pre-Commit Protection

Teams can add checks before commits or pushes to detect suspicious credentials. This can reduce the chance that a secret reaches the remote repository.

However, client-side hooks are not a complete security boundary because developers can bypass them and credentials can leak through other channels. Server-side repository scanning and secret-management controls are valuable additional layers.

Secret Management Services

For production systems, dedicated secret-management services can provide stronger controls than manually maintained .env files. These systems can support access policies, auditing, rotation workflows and controlled retrieval of credentials.

The specific service is less important than the capabilities required by the application. A small personal project may only need platform-managed environment variables, while a larger organization may need centralized secret management with fine-grained access controls.

When a .env File Is Enough

For local development, a .env file is often a practical choice. It is simple, widely supported and easy to integrate with frameworks such as Next.js and Node.js tooling.

The main requirements are that the file is protected locally, excluded from source control when appropriate and never copied into client-side code or public artifacts.

When to Use a Secret Manager

A dedicated secret-management solution becomes more useful as the number of applications, environments and people increases. Centralized management can make access control and rotation easier than distributing individual .env files.

SituationTypical approach
Personal local projectLocal .env file
Small development team.env + protected deployment variables
Production applicationHosting platform secrets or protected environment variables
Multiple production servicesCentralized secret management may be useful
Highly sensitive infrastructureDedicated secret-management and access-control systems

Do Not Store Secrets in Frontend Repositories

A frontend application that runs entirely in the browser cannot keep a secret from its users. If a credential is required by browser code, that credential should be considered public from a security perspective.

When a third-party API requires a private credential, the usual architecture is to keep that credential on a server and have the browser communicate with the server. The server can then authenticate with the third-party service without exposing its private credential to users.

Secrets and Server-Side API Routes

Frameworks such as Next.js can keep private credentials in server-side route handlers or server functions. The browser sends a request to the application, while the server uses the protected environment variable to communicate with the external service.

const apiKey = process.env.EXTERNAL_API_KEY;

export async function GET() {
  if (!apiKey) {
    return new Response("Server configuration error", {
      status: 500,
    });
  }

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

  return Response.json(await response.json());
}

The private key remains on the server while the browser receives only the response that the application chooses to expose.

Avoid Putting Secrets in URLs

Sensitive credentials should generally not be placed in URLs or query parameters. URLs can appear in browser history, proxy logs, analytics systems, monitoring tools and server logs.

Avoid:
https://api.example.com/data?api_key=secret-value

Prefer:
Authorization: Bearer secret-value

The exact authentication mechanism should follow the API's documentation, but putting secrets into URLs creates additional places where they can be recorded accidentally.

Avoid Secrets in Error Messages

Configuration errors should explain what is missing without printing the actual secret value.

if (!process.env.PAYMENT_API_KEY) {
  throw new Error("PAYMENT_API_KEY is not configured");
}

The same principle applies to exception tracking, monitoring systems and client-facing error responses. Diagnostic information should help identify the problem without exposing credentials.

A Secret Management Workflow

A practical workflow begins by identifying which configuration values are actually sensitive. Separate secrets from ordinary configuration and decide where each value should live in development, testing and production.

For local development, store credentials in an ignored .env file or another appropriate local mechanism. Commit a safe .env.example that documents required variable names without containing production secrets.

For production, provide credentials through protected deployment configuration or a dedicated secret-management service. Keep server-only values on the server and expose only intentionally public configuration to browser code.

Finally, establish a rotation procedure. When a credential is leaked, rotate it first and investigate how the exposure occurred afterward. A secret-management strategy should include recovery, not only prevention.

A Practical Secret Checklist

  • Keep real secrets out of source code.
  • Add local .env files to .gitignore when appropriate.
  • Commit a safe .env.example instead of real credentials.
  • Use separate credentials for development, testing and production.
  • Give credentials only the permissions they require.
  • Keep server-only secrets away from browser code.
  • Never use NEXT_PUBLIC_ for private values in Next.js.
  • Do not copy .env files containing secrets into Docker images.
  • Avoid printing process.env in logs.
  • Do not put credentials in URLs or query parameters.
  • Use cryptographically secure random generation for secrets you create yourself.
  • Rotate credentials immediately when they are exposed.
  • Use protected deployment variables or secret-management services for production.
  • Consider secret scanning as an additional layer of protection.

Common Secret Management Mistakes

  • Assuming that .env files are encrypted by default.
  • Committing .env to Git.
  • Deleting a leaked secret without rotating it.
  • Using production credentials during local development.
  • Putting private keys in NEXT_PUBLIC_ variables.
  • Logging API keys for debugging.
  • Printing the entire process.env object.
  • Encoding secrets with Base64 and treating the result as encryption.
  • Storing encryption keys next to encrypted configuration.
  • Using Math.random() for security-sensitive secrets.
  • Giving one credential access to more resources than necessary.
  • Pasting real credentials into issues, pull requests or chat messages.
  • Copying secrets into frontend bundles.

Frequently Asked Questions

Are .env files secure?

A .env file is normally plain text and is not encrypted by default. It can be appropriate for local development, but its security depends on file permissions, repository configuration and how the values are used.

Should I commit .env to Git?

Real .env files containing secrets should generally not be committed to Git. A safer approach is to keep the real file outside version control and commit a .env.example containing variable names and safe placeholder values.

What should I do if an API key was committed to Git?

Treat the key as exposed. Revoke or rotate the credential as soon as possible, then investigate and clean up the repository exposure according to the project's security procedures. Simply deleting the key from the latest commit is not sufficient.

Is encrypting a .env file enough to protect secrets?

Encryption can protect the contents of the file while it is stored or transferred, but the decryption key must also be protected. If an attacker can obtain both the encrypted file and its decryption key, the encryption provides little practical protection.

Can I put API keys in frontend environment variables?

Only credentials that are intentionally public should be exposed to frontend code. Anything required to remain secret must stay on the server because browser code and network requests can be inspected by users.

Should development and production use the same secrets?

They generally should not. Separate credentials reduce the impact of a compromised development machine or test environment and make it easier to rotate one environment without affecting another.

How should I generate a secure secret?

Use a cryptographically secure random generator provided by the runtime or a trusted cryptographic library. Do not use predictable values, manually invented strings or Math.random() for security-sensitive secrets.

Helpful Environment Tools

A dotenv Encryptor can help protect environment-file contents when an encrypted configuration workflow is appropriate, while a dotenv Decryptor can restore an encrypted file for an authorized development or deployment process. A Secret Generator is useful for creating strong random values for application secrets, session secrets and similar configuration. A Secret Mask Generator can help create masked representations for safe debugging or configuration displays, while an Environment Variable Generator can speed up the creation of correctly formatted environment-variable definitions.

Conclusion

Managing secrets in .env files is primarily about controlling where sensitive values exist and who can access them. A .env file is convenient for local development, but it should never be confused with a secure secret vault.

Keep real credentials out of Git, use .env.example for documentation, separate development and production credentials, apply least-privilege access and avoid exposing server-only values to browser code. In production, use protected deployment configuration or dedicated secret-management infrastructure when appropriate.

Good secret management also requires a response plan. If a credential leaks, rotate or revoke it rather than simply deleting the visible copy. Encryption, secret scanning, masking and access controls can provide additional layers of protection, but none of them replaces careful handling of credentials throughout the application's lifecycle.

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.