Back to blog
How to Set Environment Variables: Cross-Platform Guide

How to Set Environment Variables: Cross-Platform Guide

Learn how to set environment variables across Windows, macOS, Linux, Docker, & CI/CD. Includes secure setup & troubleshooting tips.

May 29, 2026by EnvManager Team
how to set environment variablesenv varsdevopssecret managementdocker environment

You're probably here because something broke in a way that felt unfair. The app runs locally, the tests pass, and then staging or production fails because DATABASE_URL, API_KEY, or NODE_ENV wasn't where the process expected it.

That's the part beginners rarely get told clearly. Learning how to set environment variables isn't mostly about memorizing commands. It's about understanding scope, lifecycle, and where a process gets its configuration from. If you get that mental model right, the commands become straightforward. If you don't, you end up chasing ghosts across shells, terminals, containers, and deployment dashboards.

Table of Contents

Why Environment Variables Are Your Project's Foundation

A familiar outage starts with a small shortcut. Someone hard-codes an API key to get local testing working, removes it before commit, and assumes the value exists in staging or production. The deploy passes. The app boots. The first request that needs that key fails.

Environment variables prevent that failure by giving configuration its own lifecycle. Code moves through your build and deploy process as one artifact. Configuration changes by scope and by environment. That split lets the same app run on a laptop, in a shared test environment, inside a short-lived CI job, or in production without editing source files every time.

The benefit is operational, not academic. A database host might be user-specific on a developer machine, injected at container start in staging, and managed centrally in production. A token might exist only for one shell session during debugging, while APP_ENV needs to be set for every process on a server. If you do not separate code from configuration, those differences get buried in scripts, commits, and tribal knowledge.

That design helps in a few concrete ways:

  • Code stays portable: You can promote the same build across environments without patching files.
  • Configuration matches its lifecycle: Temporary values stay temporary. Shared values can be managed at user, system, or platform level.
  • Secrets are less likely to leak: Credentials are kept out of the repo and out of diffs where they tend to spread.

API work makes the cost of getting this wrong obvious. Auth keys, webhook secrets, callback URLs, feature flags, and vendor-specific endpoints often change by environment and sometimes by runtime context. Good configuration discipline supports good API work, which is why these strategic API creation insights pair well with backend setup decisions.

A useful rule is simple: if a value can change without a code change, it does not belong in the codebase.

That includes secrets such as STRIPE_SECRET_KEY, but also ordinary settings like APP_ENV, LOG_LEVEL, region-specific endpoints, and feature toggles. I have seen teams protect the secrets and still hard-code the non-secret config, then spend hours debugging why one container points at staging while another points at production. The failure mode is rarely dramatic at first. It usually shows up as drift.

Security is part of this, but not the whole story. Scope and lifetime matter just as much. A value set in your current shell is not the same as a value stored for your user profile. A variable baked into an image behaves differently from one injected when the container starts. If you want the security side explained in more detail, read this guide to secrets management fundamentals. Put behavior in code. Put environment-specific values outside it.

Understanding Scope and Persistence

Most confusion about how to set environment variables comes from asking the wrong question. People ask, “What command do I run?” The better question is, “Which process should see this value, and for how long?”

A diagram illustrating the two main categories of environment variables: scope and persistence with their subcategories.

A variable can be temporary, inherited, user-specific, system-wide, or injected only when a container starts. Those aren't minor details. They determine whether your app can read the value at all.

Scope

Scope answers who can access the variable.

Here's the simplest explanation:

Scope What sees it Typical use
Current process Only the shell or app process and its children One-off testing, local launches
User level Processes started by one user Personal dev machine setup
System level All users and many processes on the machine Shared workstation or server config

A lot of developers first meet environment variables in a shell, so they assume everything is shell-local. That's no longer enough. Major developer platforms now model environment variables across multiple scopes. Domino Data Lab, for example, documents variables at compute environment, project, model, and user levels, with a deterministic evaluation order of compute environment first, then project, then user, in its environment variable management documentation. That layered model reflects how modern systems resolve conflicts predictably at scale.

Persistence

Persistence answers how long the variable lasts.

A variable might exist only until you close the terminal. It might persist for your user account across logins. It might be written for the whole system and survive reboots. That difference matters more than the command syntax.

Set the variable where the application actually starts, not where you happen to be typing.

That sounds obvious, but it's where people slip. They set a value in one terminal and expect an IDE, GUI app, cron job, or container platform to magically inherit it. Many how-to articles show commands, but fewer explain lifecycle clearly. Twilio's guide points out that the underserved angle is whether a variable is temporary for one shell session, inherited by child processes, or made permanent at the user or system level, and that terminals often must be restarted for changes to take effect in its environment variable tutorial.

The mental model that saves time

When you're deciding how to set a variable, use this checklist:

  1. Who needs it. One terminal, one app, one user, or the whole machine.
  2. How long it should live. This session only, or persist across restarts.
  3. Whether it's sensitive. If it is, avoid scattering it across local files and dashboards.
  4. Where the process starts. Terminal, IDE, container, service manager, or deployment platform.

If you answer those four questions first, you'll choose the right method far more often.

Setting Variables on Your Local Machine

Local setup is where most developers first learn how to set environment variables, and it's also where they build bad habits. The fix is simple. Use a repeatable workflow and know the difference between session-only and persistent settings.

A focused programmer typing at a computer desk, configuring environment variables on a modern workstation.

Use a check set verify workflow

Before changing anything, check whether the variable already exists. Intel's guidance recommends a cross-platform pattern: inspect the current value first, make the change, then verify it immediately. On Linux and macOS that means env | grep NAME, and on Windows that means set NAME, before and after the update, as shown in Intel's setup instructions.

That check-set-verify habit prevents two common mistakes:

  • Overwriting a value unnoticed
  • Thinking a change worked when it only applied to a different shell

Linux and macOS

On Unix-like systems, the fastest session-only option is to export a variable in your current shell.

export API_KEY="your-value"
echo $API_KEY

That value lasts for the current shell session and is inherited by child processes started from that shell. Close the terminal, and it's gone.

For persistent setup, add the export line to the right shell startup file:

  • bash often uses ~/.bashrc or ~/.bash_profile
  • zsh commonly uses ~/.zshrc
  • fish uses its own config approach rather than Bash syntax

Example for Bash or Zsh:

export API_KEY="your-value"
export DATABASE_URL="your-connection-string"

Then reload the file or open a new terminal:

source ~/.zshrc

If you're not comfortable editing shell profile files, this walkthrough on how to edit Linux files is a practical refresher.

A lot of local mistakes come from writing to the wrong file. On one machine, your interactive shell may read ~/.zshrc. On another, your login shell may depend on ~/.bash_profile. If the variable doesn't appear after you reopen the terminal, check which shell you're running before assuming the command failed.

For Linux-specific examples beyond the basics, this reference on setting environment variables on Linux gives a focused view.

After you've seen the shell-based approach once, this quick video is a good visual walkthrough:

Windows CMD and PowerShell

Windows is where scope confuses people most because there are clear differences between current-process, user, and system settings.

For a temporary CMD session:

set API_KEY=your-value
set API_KEY

That affects only the current Command Prompt process and children launched from it.

For a temporary PowerShell session:

$Env:API_KEY = "your-value"
$Env:API_KEY

That works similarly. It's immediate, but it won't persist after you close the shell.

For persistent CMD-based setup, Windows provides setx. According to Configu's Windows, Linux, and macOS guide, setx VARIABLE_NAME "value" writes a user-specific variable, while setx VARIABLE_NAME "value" /M writes a system-wide variable and requires administrator privileges. The change only applies to new Command Prompt sessions, so you need to restart the terminal or log out and back in.

Examples:

setx API_KEY "your-value"
setx API_KEY "your-value" /M

Use the first for your own account. Use the second only when every user and process on the machine should inherit the value.

Don't use system-wide variables for convenience. Use them when you mean global scope.

That distinction matters on shared development machines and servers. If a credential belongs only to one app run by one user, setting it system-wide increases the blast radius for no good reason.

Managing Variables in Containers and CI/CD Pipelines

Local shell commands are useful, but modern apps often don't start from your laptop shell. They start inside a container, from a deployment platform, or from a CI/CD job. That changes where the variable should live.

A diagram illustrating how environment variables are securely injected into modern application deployment pipelines at runtime.

Containers need runtime config

A clean container image should hold application code and dependencies, not production secrets. In practice, that means you usually pass environment variables at runtime, not bake them into the image.

Common patterns include:

  • Direct flags at launch: Pass -e NAME=value when starting a container.
  • Env files for local composition: Keep project-specific values in an env file used during local runs.
  • Dockerfile defaults: Use ENV only for non-sensitive defaults that are safe to ship with the image.

That separation is the same operational shift described earlier. Build once, inject config later. It keeps one image usable across development, staging, and production.

If you work with frontend frameworks that blur build-time and runtime configuration, this guide to mastering Next.js env variables is worth reading because Next.js has a particularly sharp distinction between server-only and browser-exposed values.

Kubernetes and deployment platforms

Kubernetes makes the same concept more explicit. Non-sensitive values usually belong in ConfigMaps. Sensitive values belong in Secrets. The key idea isn't the object name. It's the boundary between app artifact and deployment-specific configuration.

A strong deployment workflow also needs environment-specific overrides. That's where many beginner tutorials stop too early. In real systems, teams need development, QA, staging, and production to carry different values while preserving the same app logic.

Microsoft's Power Pages documentation reflects that pattern by treating environment variables as values defined once and then selected or updated separately per environment in its guidance for environment-specific configuration. That's the right mental model for multi-environment systems.

This is also where local Docker setups often diverge from production. If you're managing layered compose files for different targets, this article on Docker Compose multiple file workflows is a practical reference.

CI/CD needs explicit injection

Pipelines fail for the same reason local deploys fail. People assume a job runner has the same environment they had on their machine.

It doesn't.

A reliable CI/CD setup does three things:

  1. Stores variables in the pipeline or platform secret store
  2. Injects them only into the jobs that need them
  3. Keeps build artifacts separate from sensitive runtime values

That matters because CI jobs are ephemeral. They come and go. You can't depend on a shell profile from yesterday's machine state.

A good rule for pipelines is straightforward:

  • Build steps should receive only what they need.
  • Deploy steps should inject environment-specific values for the target system.
  • Sensitive values should never be copied into source files just to “make the pipeline work.”

If you're troubleshooting a broken deployment, always ask where the app was started. The right answer is rarely “the terminal where I tested it earlier.”

From .env Files to Secure Team Workflows

A common failure looks like this. A developer has the app working locally with a .env file, the container builds, the staging deploy passes, and then production fails because one value was copied from the wrong place six days ago.

That happens because .env solves a local startup problem. Team workflows have a different job. They need to control scope, persistence, and access across people and environments.

A comparison chart showing the pros and cons of using .env files versus secure workflow management tools.

Why .env works until it doesn't

A .env file is a flat list of key-value pairs. That makes it useful for local development, especially when the values should exist only on one machine and only for one project checkout.

The trouble starts when people use the same file format for values with very different lifecycles. Some settings belong to a single shell session. Some should persist on one developer machine. Some belong to a shared staging environment. Some are production secrets that should only exist at runtime inside a specific service process.

A plain .env file does not enforce any of that. It does not tell you who changed a value, who should have access, which environment owns it, or whether it was meant to be temporary or long-lived.

That gap creates familiar problems:

  • Secrets get distributed manually: copied into chat, tickets, notes, or screenshots
  • Environment drift appears unnoticed: one developer updates a local file, another keeps using the old value
  • Production scope leaks into local scope: someone tests with real credentials on a laptop, then forgets they are still there
  • Committed secrets stay dangerous after deletion: removing the file from the repo does not remove it from history

A useful rule is simple. .env is a local convenience file, not a team control plane.

What a secure workflow looks like

A team setup works better when it matches the lifecycle of the value.

Local non-sensitive defaults can live in a developer-managed file. Shared application settings should come from the deployment platform or configuration system for that environment. Secrets should come from a secret store and be injected only into the process, job, or container that needs them.

That approach fixes more than secrecy. It also reduces confusion about persistence. Developers stop asking whether a value should live in a shell profile, a repo file, a CI variable store, a Kubernetes secret, or a cloud secret manager. Each one has a different scope, and mixing them is where mistakes happen.

In practice, strong team workflows usually have these traits:

Ad hoc workflow Secure workflow
Send .env over chat Retrieve values from one approved system
Copy values into dashboards by hand Apply values through repeatable deployment steps
One file used for every environment Separate values by local, staging, and production scope
Everyone can read production secrets Access is limited by role and environment
Changes are hard to trace Updates are logged and easier to review

The biggest improvement is operational clarity. A developer knows which values are local-only, which are shared by the team, and which should never leave the runtime environment.

What to keep doing and what to stop doing

Some habits still make sense:

  • Use .env for local development only: keep it for machine-local, non-production configuration
  • Commit a template, not the actual file: .env.example helps new developers without exposing secrets
  • Name variables clearly: DATABASE_URL and REDIS_PASSWORD are better than vague abbreviations
  • Document lifecycle along with the key: note whether a value is session-only, machine-persistent, environment-specific, or runtime-injected

Other habits cause incidents:

  • Do not commit real secrets
  • Do not reuse production credentials in local development
  • Do not treat copied .env files as the source of truth
  • Do not let long-lived secrets sit on laptops unless there is a clear reason

I have seen teams lose hours on bugs that were really scope bugs. The code was fine. The wrong value existed in the wrong place for too long.

That is why mature workflows separate convenience from control. Keep .env where it helps, on one machine, for one developer, in local work. Move shared and sensitive values into systems that match their real scope and lifecycle.

Examples and Common Troubleshooting

Once a variable is set, your app still has to read it correctly. Developers often then discover that “set” and “available to this process” are not the same thing.

Reading variables in code

A few common examples:

Node.js

const apiKey = process.env.API_KEY;
console.log(apiKey);

Python

import os

api_key = os.environ.get("API_KEY")
print(api_key)

Go

package main

import (
    "fmt"
    "os"
)

func main() {
    apiKey := os.Getenv("API_KEY")
    fmt.Println(apiKey)
}

Java

public class Main {
    public static void main(String[] args) {
        String apiKey = System.getenv("API_KEY");
        System.out.println(apiKey);
    }
}

.NET

using System;

class Program
{
    static void Main()
    {
        Environment.SetEnvironmentVariable("API_KEY", "value");
        Console.WriteLine(Environment.GetEnvironmentVariable("API_KEY"));
    }
}

For .NET specifically, Microsoft documents that Environment.SetEnvironmentVariable(String, String) for a current-process setting creates, modifies, or deletes an environment variable stored only in the current process. That makes it useful for launch-time configuration, but not for persistence across terminals or reboots, as explained in Microsoft's .NET API documentation.

Why a variable isn't showing up

When a variable seems to be missing, the root cause is usually one of these:

  • Wrong scope: You set it in one shell, but the app started somewhere else.
  • Terminal restart required: Persistent Windows changes often don't appear in already-running terminals.
  • Wrong profile file: You edited .bashrc, but your shell reads .zshrc.
  • Different launch path: The app starts from an IDE, service, or container that doesn't inherit your shell environment.

A quick troubleshooting checklist helps:

  1. Print the value in the same context where the app runs
  2. Confirm the process start point
  3. Reopen the terminal or restart the app
  4. Check for typos in the variable name
  5. Verify whether the variable was meant to be temporary or persistent

If the app can't read the variable, inspect the environment of the running process, not the environment you think it should have.

Special cases that trip people up

Quoting and special characters

If a value contains spaces or shell-sensitive characters, quote it when setting it. Unquoted values can be split or interpreted by the shell.

Child process assumptions

A child process inherits the environment of its parent process. A sibling process does not. That's why “it worked in this terminal” tells you very little about another process tree.

Overlapping names

Using the same variable name across scripts, shells, and deployment systems can lead to accidental overrides. Keep names consistent, but also be intentional about where each one is defined.

Temporary programmatic values

Setting a variable inside code can be useful for tests or one process launch. It won't magically persist outside that process unless you explicitly write it to an appropriate persistent scope using the operating system's mechanisms.

If you remember only one troubleshooting habit, make it this one: always debug environment variables at the boundary where the process starts.


If your team has outgrown scattered .env files, manual dashboard edits, and secrets shared over chat, EnvManager gives you a cleaner path. You can import existing variables, organize them by environment, sync them to local machines or CI/CD with a CLI, and keep access controlled with encryption, versioning, and audit trails. It's the kind of setup that removes a lot of routine config mistakes before they turn into production problems.

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Start your free trial

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.