Back to Blog
Tutorial9 min read2026-07-04

How to Manage Environment Variables Across Deployments

How to handle environment variables cleanly across local, staging, and production — separating config from secrets, avoiding the .env-in-git disaster, and keeping environments in sync.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Manage Environment Variables Across Deployments

Environment variables are how you separate configuration from code — a core principle of the [Twelve-Factor App](https://12factor.net/config). They're also where a lot of production incidents start: a missing variable, a secret committed to Git, or staging quietly drifting from production. This tutorial covers doing it right.

Why environment variables at all?

The same container image should run in development, staging, and production without modification. What changes between those is *config*: database URLs, API keys, feature flags, log levels. Baking those into the image means rebuilding for every environment and risks leaking production secrets into your registry. Reading them from the environment keeps one artifact, many configurations.

Config vs. secrets — they're not the same

A distinction that matters more than people think:

TypeExamplesSensitivityWhere it lives
ConfigLOG_LEVEL, PORT, FEATURE_X=trueLowPlain env vars
SecretsDB passwords, API keys, JWT signing keysHighEncrypted secret store

Both arrive in your app as environment variables, but they should be *managed* differently. Secrets need encryption at rest, restricted access, and audit trails. Config can be more freely shared and version-controlled (as templates).

Step 1: Use a .env file locally — and gitignore it

For local development, a .env file is fine. The critical rule: never commit it.

# .gitignore
.env
.env.local
.env.*.local

Commit a template instead so teammates know what's needed:

# .env.example (safe to commit — no real values)
DATABASE_URL=
REDIS_URL=
STRIPE_SECRET_KEY=
LOG_LEVEL=info

If you've ever committed a real secret, rotate it immediately — Git history is forever, and scrapers find committed keys within minutes.

Step 2: Read variables defensively

Fail loudly at startup if a required variable is missing, rather than crashing mysteriously mid-request.

function requireEnv(name) {
 const val = process.env[name];
 if (!val) {
 throw new Error(`Missing required env var: ${name}`);
 }
 return val;
}

const config = {
 databaseUrl: requireEnv('DATABASE_URL'),
 logLevel: process.env.LOG_LEVEL || 'info', // optional with default
};

Validating config at boot is one of the highest-ROI defensive patterns you can adopt. A pod that refuses to start with a clear error is far better than one that serves 500s.

Step 3: Keep environments organized

The goal is parity: staging should differ from production only in values, not in *which* variables exist. A common failure is adding a new variable to production but forgetting staging, so the next staging deploy breaks.

Strategies:

  • Maintain one canonical list of variable *names* (your .env.example).
  • Set values per environment in your platform's config UI/API.
  • Review diffs between environments before major releases.

Step 4: Handle changes without downtime

Changing an environment variable usually requires restarting the app to pick it up — most processes read env vars once at startup. On a well-run platform, updating a variable triggers a rolling restart so there's no outage. Plan for this: a variable change is a deploy, not a free action.

For truly dynamic values you don't want to redeploy for, use a runtime config source (a settings table or feature-flag service) rather than environment variables.

Managing env vars on PandaStack

PandaStack lets you set environment variables per app through the dashboard, and they're delivered to your container's runtime environment at startup. Secrets are handled as part of the platform's secret management rather than ending up in your image or your Git history.

A couple of platform-specific notes worth knowing:

  • When you attach a managed database, PandaStack injects DATABASE_URL automatically — you don't set it by hand, and you shouldn't override it.
  • Certain platform-reserved variables are protected so a stray value in your config can't break the runtime wiring.
  • Updating a variable rolls out as a new deployment, so your change takes effect cleanly without you SSHing into anything.

This keeps the same mental model across environments: your code always reads process.env.SOMETHING, and the values come from the platform, not from a file you might accidentally commit.

Common mistakes

  • ❌ Committing .env with real secrets
  • ❌ Different variable *sets* across environments (drift)
  • ❌ Silent fallback to a default for a required secret
  • ❌ Logging full config (which logs secrets) at startup
  • ✅ Gitignore .env, commit .env.example
  • ✅ Validate required vars at boot
  • ✅ Keep variable names identical across environments
  • ✅ Treat secrets as encrypted, access-controlled data

References

  • [The Twelve-Factor App: Config](https://12factor.net/config)
  • [OWASP: Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
  • [Kubernetes: ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/)
  • [dotenv documentation](https://github.com/motdotla/dotenv)

Set your environment variables once and deploy across environments cleanly on PandaStack — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also