Back to Blog
Guide

Environment Variables and Secrets Management: A Developer's Guide

Daniel Brooks8 min read
Environment Variables and Secrets Management: A Developer's Guide

Every application has configuration that shouldn't live in code: database URLs, API keys, JWT secrets, third-party credentials. Environment variables are the standard solution. But managing them securely across development, preview, and production environments is where most teams cut corners — and where breaches happen.

This guide covers the fundamentals of environment variables and secrets management: what they are, how to handle them locally and in production, stack-specific examples, and the security mistakes that compromise applications daily.

What Are Environment Variables?

Environment variables are key-value pairs set outside your application code that configure its behavior at runtime. They are the standard mechanism for separating configuration from code.

The Twelve-Factor App methodology — a widely adopted framework for building production-grade software — defines configuration as "everything that is likely to vary between deploys." Environment variables are the prescribed mechanism for that separation. The core principle: code should be identical across environments; configuration should differ.

Without environment variables, developers reach for two inferior alternatives. Config files committed to the repository expose credentials to anyone with repository access. Hardcoded values are even worse — they require code changes to update, cannot differ per environment, and become permanent fixtures in git history.

Environment variables solve both problems. They keep credentials out of source control, allow identical code to behave differently across environments, and can be updated without redeploying your application.

Environment Variables vs. Secrets

Not all environment variables are secrets. Understanding this distinction helps you apply the right level of protection to each variable.

Environment variables configure application behavior but carry no security risk if exposed:

  • PORT — the port your server listens on
  • NODE_ENV — controls debug mode, logging verbosity
  • LOG_LEVEL — sets log output detail
  • API_URL — the base URL for a public API
  • MAX_WORKERS — concurrency settings

Secrets are environment variables that grant access to sensitive systems or data. Their exposure is a security incident:

  • DATABASE_URL — full access to your database, often including credentials
  • API_KEY — authenticates with third-party services
  • JWT_SECRET — signs and verifies authentication tokens
  • STRIPE_SECRET_KEY — processes real financial transactions
  • SMTP_PASSWORD — sends email as your domain

Secrets require additional protection beyond what regular environment variables need: masking in UI displays, encryption at rest, access controls, and rotation schedules. Treating all environment variables as secrets is overly cautious. Treating secrets as ordinary variables is how data gets compromised.

Local Development: .env Files

During local development, environment variables are typically managed through .env files. These are plain text files in your project root that your application loads at startup.

Setting Up .env

A typical .env file for a web application:

bash
# .env (NEVER commit this file)
DATABASE_URL=postgresql://localhost:5432/myapp_dev
API_KEY=dev_key_12345
JWT_SECRET=local_dev_secret_not_used_in_production
NODE_ENV=development
PORT=3000
REDIS_URL=redis://localhost:6379

Use obvious placeholder values for local secrets. This makes it immediately clear that these credentials are for development only, and a real production secret would never contain dev_key or local_dev in its value.

Loading .env Files

Each language ecosystem has a standard way to load .env files.

Node.js — Use the dotenv package or the built-in --env-file flag available since Node 20:

javascript
// Using dotenv (all Node versions)
require('dotenv').config();

// Using native flag (Node 20+)
// node --env-file=.env server.js

Python — Use python-dotenv:

python
from dotenv import load_dotenv
import os

load_dotenv()

database_url = os.getenv('DATABASE_URL')

Go — Use godotenv:

go
import (
    "github.com/joho/godotenv"
    "os"
)

func init() {
    godotenv.Load()
}

databaseURL := os.Getenv("DATABASE_URL")

All three approaches follow the same pattern: load the file early in application startup, then read variables using the platform's standard environment access method. In production, the platform injects variables directly into the process environment, so your code does not change between development and production.

.gitignore Is Not Optional

Add these lines to your .gitignore before you create any .env files:

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

This cannot be overstated: if .env ends up in your git history, your secrets are compromised regardless of whether you later add it to .gitignore. Removing a file from .gitignore does not erase it from history. Every team member who cloned the repository before the fix already has the credentials.

If you commit secrets to git by accident, rotate them immediately. Do not wait to see if anyone noticed. Assume the credentials are already in the hands of someone with bad intentions. Services like GitHub automatically scan pushed commits for common secret patterns and alert maintainers, but automated scanning is not a substitute for not committing secrets in the first place.

Production: Platform Environment Variables

Local .env files do not belong in production. The production mechanism is environment variables set directly on the hosting platform, injected into your application process at startup.

Setting Variables on Out Plane

Out Plane provides two methods for managing environment variables in your application.

Adding variables one at a time:

  1. Navigate to your application in console.outplane.com
  2. Open the Environment Variables section
  3. Click Add and enter the key and value
  4. Select the environment scope: Development, Preview, or Production
  5. Save — the variable is available on next deployment

Bulk entry with Raw Edit:

For migrating a full .env file, Raw Edit is faster than adding variables one by one:

  1. Click Raw Edit
  2. Paste your variables in KEY=value format, one per line
  3. Click Apply to save all variables at once
DATABASE_URL=postgresql://user:pass@host:5432/myapp_prod
REDIS_URL=redis://cache.internal:6379
API_KEY=sk_live_abc123xyz
JWT_SECRET=a-long-random-string-at-least-32-chars
NODE_ENV=production

All values are masked by default in the UI. The actual values are not visible after saving unless you explicitly reveal them. This prevents casual exposure when sharing screens or writing documentation.

Environment-Specific Configuration

Out Plane separates environments into three tiers, each with a distinct purpose:

  • Development — local-adjacent builds, debug logging enabled, development database, test API keys
  • Preview — branch deployments for pull requests, staging database, test credentials that mirror production behavior
  • Production — live application, production database, real API keys, no debug output

When you set an environment variable, you select which environments receive it. This scoping is the mechanism that prevents production credentials from leaking into development or preview deployments. A DATABASE_URL set to your production database should only be scoped to Production. Development and Preview get separate DATABASE_URL values pointing to their own isolated databases.

This separation also allows you to use test API keys — from Stripe, SendGrid, Twilio, or any other provider — in Development and Preview environments. Real transactions never occur in non-production environments when the credentials are properly scoped.

Common Environment Variables by Stack

Different frameworks expect different variable names. These are standard configurations by stack.

Node.js / Express

NODE_ENV=production
PORT=8080
DATABASE_URL=postgresql://user:pass@host:5432/db
SESSION_SECRET=long-random-value-min-32-chars
REDIS_URL=redis://host:6379
CORS_ORIGIN=https://yourapp.com

Python / Django

DJANGO_SETTINGS_MODULE=myapp.settings.production
SECRET_KEY=long-random-django-secret-key
DATABASE_URL=postgresql://user:pass@host:5432/db
ALLOWED_HOSTS=myapp.outplane.app,yourdomain.com
DEBUG=False

Python / FastAPI

DATABASE_URL=postgresql://user:pass@host:5432/db
SECRET_KEY=long-random-value-for-jwt-signing
CORS_ORIGINS=https://yourapp.com
ENVIRONMENT=production

Go

DATABASE_URL=postgresql://user:pass@host:5432/db
PORT=8080
JWT_SECRET=long-random-value
ENVIRONMENT=production

Ruby on Rails

RAILS_ENV=production
SECRET_KEY_BASE=long-random-rails-secret
DATABASE_URL=postgresql://user:pass@host:5432/db
RAILS_SERVE_STATIC_FILES=true

For deploying these stacks to Out Plane, the deploy fullstack app from scratch guide covers the full process from repository to live URL, including environment variable configuration for each framework.

Security Best Practices

Handling environment variables correctly prevents the most common categories of credential exposure. These practices are not optional for production applications.

Never Commit Secrets to Git

Your .gitignore is the first line of defense. Add .env before creating it. For additional protection, use pre-commit hooks to scan for secrets before they reach the repository.

git-secrets from AWS Labs blocks commits containing patterns that match common credential formats:

bash
brew install git-secrets
git secrets --install
git secrets --register-aws

For broader coverage, pre-commit with detect-secrets scans for a wider range of patterns including high-entropy strings that resemble API keys.

If secrets are already in your git history, scan the entire history before rotating credentials:

bash
# Using truffleHog
trufflehog git file://. --since-commit HEAD~100

# Using GitGuardian CLI
ggshield secret scan repo .

Principle of Least Privilege

Each environment should receive only the credentials it actually needs. Development environments do not need access to production databases. Preview deployments do not need live payment processor credentials.

Separate API keys per environment also limits the blast radius of a compromise. If a development API key leaks, you revoke it without affecting production. If you use the same key everywhere, revoking it takes down all environments simultaneously.

Most third-party services — Stripe, Twilio, SendGrid, GitHub Apps — provide separate test and production API keys for exactly this reason. Use them.

Secret Rotation

Credentials should change on a regular schedule, regardless of whether a breach is suspected. A rotation schedule limits the window of exposure for any credential that has been unknowingly compromised.

Minimum rotation intervals by credential type:

  • API keys: 90 days
  • Database passwords: 180 days
  • JWT signing secrets: 90 days (requires token invalidation strategy)
  • OAuth secrets: 180 days

Rotate immediately — not on schedule — when any of these events occur: a team member with credential access leaves, a repository becomes public, a build log may have exposed a value, or a third-party vendor reports a breach.

When rotating a JWT_SECRET, active sessions signed with the old key become invalid. Plan the rotation during low-traffic periods or implement a key transition period where both old and new keys are valid simultaneously.

Access Control

Not every team member needs to see production secrets. Limit visibility to those who require it for their work. Out Plane supports Owner and Member roles — use them to restrict who can view and edit production environment variables.

Audit who has viewed secrets when investigating incidents. The fewer people with routine access to production credentials, the smaller your investigation scope when something goes wrong.

Common Mistakes

These are the mistakes that cause real security incidents. Each has a straightforward fix.

1. Committing .env to Git

This is the most common source of credential exposure. A repository that was once public, even briefly, should be treated as fully compromised.

Fix: add .env to .gitignore before creating the file. If already committed, remove it from history with git filter-branch or BFG Repo Cleaner, then rotate every secret in that file.

2. Hardcoding Secrets in Source Code

javascript
// Wrong — secret is in source code, visible to anyone with repo access
const stripe = require('stripe')('sk_live_abc123xyz');
const dbUrl = 'postgresql://admin:password123@prod-db:5432/myapp';

// Correct — value comes from the environment at runtime
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const dbUrl = process.env.DATABASE_URL;

Hardcoded values appear in git history permanently. They cannot be rotated without a code change and deployment. They are visible to every developer who has ever cloned the repository.

3. Using the Same Credentials Everywhere

A single DATABASE_URL shared across development, preview, and production means a developer testing a destructive migration locally runs it against your production database. This happens.

Each environment needs its own database instance with its own credentials. The same applies to third-party API keys, email service credentials, and payment processor keys. The marginal cost of separate credentials is zero. The cost of a production incident caused by credential sharing is not.

4. Logging Secrets

javascript
// Wrong — DATABASE_URL contains credentials in the connection string
console.log('Connecting to database:', process.env.DATABASE_URL);

// Wrong — logs all environment variables including secrets
console.log('Environment:', process.env);

// Correct — log only what you need without sensitive values
console.log('Database connection established');
console.log('Environment:', process.env.NODE_ENV);

Application logs are often accessible to more people than production secrets. If your logs contain DATABASE_URL, every developer with log access has database credentials. Review your startup and error handling code for any logging that outputs environment variable values.

5. Passing Secrets via CLI Arguments

bash
# Wrong — visible to anyone running `ps aux` on the same host
node server.js --db-password=secret123

# Correct — set as environment variable before the process starts
DATABASE_PASSWORD=secret123 node server.js

CLI arguments are visible in the system process list. On shared infrastructure or container hosts where multiple processes run, this is a meaningful exposure risk. Environment variables set before process startup are not exposed this way.

Advanced: Dedicated Secret Management Tools

Platform environment variables are appropriate for most applications. As your security requirements grow, dedicated secret management tools provide capabilities that platform variables cannot: dynamic secret generation, fine-grained access policies, automatic rotation, and audit logs per secret access.

Consider moving to dedicated tooling when:

  • You have compliance requirements (SOC 2, ISO 27001, HIPAA) that require detailed audit trails
  • Multiple services need the same secrets, and distributing them via platform variables creates management overhead
  • You need automatic secret rotation integrated with your application logic
  • Your security team requires approval workflows before secrets can be accessed

The main options:

HashiCorp Vault — open source, self-hosted, generates dynamic credentials with short TTLs. Highest operational overhead but maximum control.

AWS Secrets Manager — managed service, integrates tightly with AWS IAM, supports automatic rotation for RDS and Redshift. Best choice if you're already on AWS infrastructure.

Doppler — SaaS secret manager, integrates with most PaaS platforms including Out Plane via environment sync. Low operational overhead, good developer experience.

1Password Secrets Automation — extends 1Password to application secrets with fine-grained access control and developer-friendly tooling.

For most teams under 50 engineers, platform environment variables with proper scoping and a rotation schedule cover the security requirements. Move to dedicated tooling when the operational complexity of your secrets grows beyond what a platform UI manages well.

For applications with data residency requirements, see the GDPR-compliant hosting in Europe guide for how infrastructure choices affect where secrets and data are processed.

Environment Variable Checklist

Before deploying any application to production, verify each item:

  • .env is in .gitignore and not tracked by git
  • No secrets appear in source code files
  • Different credentials are set for each environment (Development, Preview, Production)
  • All production secrets are set in the platform environment variables UI
  • Secret values are masked in the platform UI
  • A rotation schedule is defined for each secret type
  • Pre-commit hooks scan for accidental secret commits
  • Team members have appropriate access levels (Owner vs. Member)
  • Application logs do not output secret values
  • Test API keys are used in non-production environments

Run through this checklist before deploying a new application and before onboarding new team members who will have access to production configuration.

Summary

Environment variables separate configuration from code. Secrets are a subset of environment variables that require additional protection — masking, rotation, access control, and environment scoping.

For local development, use .env files that are excluded from version control. For production, use your platform's environment variable management. Out Plane provides scoped environment variables for Development, Preview, and Production environments, with masking enabled by default and Raw Edit mode for bulk configuration.

The practices that prevent most incidents are simple: keep secrets out of git, use different credentials per environment, rotate on a schedule, and limit who can see production values.

Ready to configure your application? Set your environment variables in console.outplane.com and deploy with confidence.


Tags

environment-variables
secrets
security
configuration
devops
guide

Start deploying in minutes

Connect your GitHub repository and deploy your first application today. $20 free credit. No credit card required.