"@context": "https://schema.org",<br> "@type": "Article",<br> "headline": "Environment Variables Best Practices: The Complete Developer Guide (2026)",<br> "description": "Learn how to manage environment variables correctly in 2026. Covers .env files, secrets management, Docker, CI/CD, production best practices, and tools to validate your config.",<br> "datePublished": "2026-03-25",<br> "author": {<br> "@type": "Organization",<br> "name": "DevToolkit"<br> }<br> })} /></p> <p>Environment variables are one of those foundational concepts that every developer uses but few get right. A leaked <code>DATABASE_URL</code> in a public repository has caused real production incidents. A missing <code>API_KEY</code> in a new developer's environment has caused hours of "why doesn't it work on my machine" debugging. A hardcoded staging URL shipped to production has silently corrupted data.</p> <p>This guide covers everything you need to know about environment variables in 2026 — from the basics of <code>.env</code> files to production secrets management and validation patterns.</p> <h2> <a name="what-are-environment-variables" href="#what-are-environment-variables" class="anchor"> </a> What Are Environment Variables? </h2> <p>Environment variables are key-value pairs stored in the operating system's environment, accessible to any process running in that environment. In Unix:<br> </p> <div class="highlight"><pre class="highlight shell"><code><span class="nb">export </span><span class="nv">DATABASE_URL</span><span class="o">=</span><span class="s2">"postgres://user:pass@localhost/mydb"</span> <span class="nb">export </span><span class="nv">NODE_ENV</span><span class="o">=</span><span class="s2">"production"</span> <span class="nb">export </span><span class="nv">PORT</span><span class="o">=</span><span class="s2">"3000"</span> </code></pre></div> <p></p> <p>Your application reads them at runtime:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="err">#</span> <span class="nx">Node</span><span class="p">.</span><span class="nx">js</span> <span class="kd">const</span> <span class="nx">dbUrl</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nf">parseInt</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="dl">"</span><span class="s2">3000</span><span class="dl">"</span><span class="p">);</span> <span class="err">#</span> <span class="nx">Python</span> <span class="k">import</span> <span class="nx">os</span> <span class="nx">db_url</span> <span class="o">=</span> <span class="nx">os</span><span class="p">.</span><span class="nx">environ</span><span class="p">[</span><span class="dl">"</span><span class="s2">DATABASE_URL</span><span class="dl">"</span><span class="p">]</span> <span class="nx">port</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="nx">os</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(</span><span class="dl">"</span><span class="s2">PORT</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">3000</span><span class="dl">"</span><span class="p">))</span> <span class="err">#</span> <span class="nx">Go</span> <span class="k">import</span> <span class="dl">"</span><span class="s2">os</span><span class="dl">"</span> <span class="nx">dbURL</span> <span class="p">:</span><span class="o">=</span> <span class="nx">os</span><span class="p">.</span><span class="nc">Getenv</span><span class="p">(</span><span class="dl">"</span><span class="s2">DATABASE_URL</span><span class="dl">"</span><span class="p">)</span> <span class="nx">port</span> <span class="p">:</span><span class="o">=</span> <span class="nx">os</span><span class="p">.</span><span class="nc">Getenv</span><span class="p">(</span><span class="dl">"</span><span class="s2">PORT</span><span class="dl">"</span><span class="p">)</span> </code></pre></div> <p></p> <p>The critical insight: environment variables let you change behavior without changing code. The same binary can connect to a development database locally and a production database in deployment — just by changing the environment.</p> <h2> <a name="the-12factor-app-principle-config-in-the-environment" href="#the-12factor-app-principle-config-in-the-environment" class="anchor"> </a> The 12-Factor App Principle: Config in the Environment </h2> <p>The <a href="https://12factor.net/config">12-Factor App methodology</a> (still the canonical reference for modern app architecture) states that config — anything that varies between deployments — should be stored in the environment, not in code.</p> <p>The litmus test: could you open-source your code right now without exposing credentials? If yes, your config is properly separated from code. If no, something is wrong.</p> <p>Things that belong in environment variables:</p> <ul> <li>Database connection strings and credentials</li> <li>API keys for external services (Stripe, SendGrid, AWS)</li> <li>JWT secrets and signing keys</li> <li>Service URLs that differ between environments (staging vs production)</li> <li>Feature flags (basic on/off)</li> <li>Server configuration (PORT, LOG_LEVEL)</li> </ul> <p>Things that do NOT belong in environment variables:</p> <ul> <li>Large configuration files (use file-based config, reference the path via env var)</li> <li>Binary data or certificates (use file references)</li> <li>Business logic or application behavior</li> </ul> <h2> <a name="env-files-the-development-standard" href="#env-files-the-development-standard" class="anchor"> </a> .env Files: The Development Standard </h2> <p>For local development, <code>.env</code> files are the universal standard. A <code>.env</code> file is a plain text file at the project root:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="c"># .env </span><span class="py">DATABASE_URL</span><span class="p">=</span><span class="s">postgres://dev_user:dev_pass@localhost:5432/myapp_dev</span> <span class="py">REDIS_URL</span><span class="p">=</span><span class="s">redis://localhost:6379</span> <span class="py">API_KEY</span><span class="p">=</span><span class="s">sk_test_dev_key_here</span> <span class="py">JWT_SECRET</span><span class="p">=</span><span class="s">dev-secret-change-in-production</span> <span class="py">PORT</span><span class="p">=</span><span class="s">3000</span> <span class="py">NODE_ENV</span><span class="p">=</span><span class="s">development</span> <span class="py">LOG_LEVEL</span><span class="p">=</span><span class="s">debug</span> </code></pre></div> <p></p> <p>The <code>dotenv</code> library (available in every major language) loads this file into <code>process.env</code> at startup:<br> </p> <div class="highlight"><pre class="highlight shell"><code><span class="c"># Node.js</span> npm <span class="nb">install </span>dotenv <span class="c"># In app entry point (before any other imports that need env vars)</span> require<span class="o">(</span><span class="s1">'dotenv'</span><span class="o">)</span>.config<span class="o">()</span> <span class="c"># or (ES modules)</span> import <span class="s1">'dotenv/config'</span> </code></pre></div> <p></p> <p></p> <div class="highlight"><pre class="highlight python"><code><span class="c1"># Python </span><span class="n">pip</span> <span class="n">install</span> <span class="n">python</span><span class="o">-</span><span class="n">dotenv</span> <span class="kn">from</span> <span class="n">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span> <span class="nf">load_dotenv</span><span class="p">()</span> <span class="c1"># Loads .env into os.environ </span></code></pre></div> <p></p> <h3> <a name="critical-rule-never-commit-env-to-git" href="#critical-rule-never-commit-env-to-git" class="anchor"> </a> Critical Rule: Never Commit .env to Git </h3> <p>Add <code>.env</code> to your <code>.gitignore</code> immediately:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="c"># .gitignore </span><span class="err">.env</span> <span class="err">.env.local</span> <span class="err">.env.*.local</span> </code></pre></div> <p></p> <p>If you accidentally committed a <code>.env</code> file with real credentials:</p> <ul> <li><strong>Rotate the credentials immediately</strong> — treat them as compromised</li> <li>Remove the file from git history using <code>git filter-branch</code> or BFG Repo-Cleaner</li> <li>Force-push the cleaned history</li> <li>Alert your team that the old commits are invalidated</li> </ul> <p>Even after removal from history, if the repo was ever public, assume the credentials were scraped by bots within minutes. Always rotate.</p> <h3> <a name="the-envexample-pattern" href="#the-envexample-pattern" class="anchor"> </a> The .env.example Pattern </h3> <p>Commit a <code>.env.example</code> file instead — it shows which variables are needed without real values:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="c"># .env.example — COMMIT THIS FILE </span><span class="py">DATABASE_URL</span><span class="p">=</span><span class="s">postgres://user:password@localhost:5432/dbname</span> <span class="py">REDIS_URL</span><span class="p">=</span><span class="s">redis://localhost:6379</span> <span class="py">API_KEY</span><span class="p">=</span><span class="s">your_api_key_here</span> <span class="py">JWT_SECRET</span><span class="p">=</span><span class="s">your_jwt_secret_here</span> <span class="py">PORT</span><span class="p">=</span><span class="s">3000</span> <span class="py">NODE_ENV</span><span class="p">=</span><span class="s">development</span> </code></pre></div> <p></p> <p>New developers clone the repo, copy <code>.env.example</code> to <code>.env</code>, and fill in their local values. This pattern is so universal that most project READMEs include: <code>cp .env.example .env</code> as the first setup step.</p> <h2> <a name="multiple-environment-files" href="#multiple-environment-files" class="anchor"> </a> Multiple Environment Files </h2> <p>Modern frameworks support multiple <code>.env</code> files with priority ordering:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="err">.env</span> <span class="c"># Defaults for all environments </span><span class="err">.env.local</span> <span class="c"># Local overrides (never committed) </span><span class="err">.env.development</span> <span class="c"># Development-specific </span><span class="err">.env.test</span> <span class="c"># Test-specific (committed, no real secrets) </span><span class="err">.env.production</span> <span class="c"># Production defaults (committed, no real secrets) </span></code></pre></div> <p></p> <p>Next.js, Vite, and Create React App all support this pattern. The loading priority (last wins) is typically: <code>.env</code> → <code>.env.{NODE_ENV}</code> → <code>.env.local</code> → <code>.env.{NODE_ENV}.local</code>.</p> <p>For tests, a <code>.env.test</code> with fake credentials prevents test runs from hitting real services:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="c"># .env.test — safe to commit </span><span class="py">DATABASE_URL</span><span class="p">=</span><span class="s">postgres://test:test@localhost:5432/myapp_test</span> <span class="py">REDIS_URL</span><span class="p">=</span><span class="s">redis://localhost:6379</span> <span class="py">STRIPE_SECRET_KEY</span><span class="p">=</span><span class="s">sk_test_fake_key_for_testing</span> <span class="py">JWT_SECRET</span><span class="p">=</span><span class="s">test-secret-not-production</span> </code></pre></div> <p></p> <h2> <a name="validation-fail-fast-on-missing-variables" href="#validation-fail-fast-on-missing-variables" class="anchor"> </a> Validation: Fail Fast on Missing Variables </h2> <p>One of the most common bugs: an environment variable is missing, the app starts anyway, and fails mysteriously 10 minutes later when a code path that uses it is hit. Validate your environment at startup.</p> <h3> <a name="manual-validation" href="#manual-validation" class="anchor"> </a> Manual Validation </h3> <p></p> <div class="highlight"><pre class="highlight javascript"><code><span class="c1">// Node.js — validate required vars at startup</span> <span class="kd">const</span> <span class="nx">required</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">DATABASE_URL</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">JWT_SECRET</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">STRIPE_SECRET_KEY</span><span class="dl">'</span><span class="p">];</span> <span class="kd">const</span> <span class="nx">missing</span> <span class="o">=</span> <span class="nx">required</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="nx">key</span> <span class="o">=></span> <span class="o">!</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">[</span><span class="nx">key</span><span class="p">]);</span> <span class="k">if </span><span class="p">(</span><span class="nx">missing</span><span class="p">.</span><span class="nx">length</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="s2">`Missing required environment variables: </span><span class="p">${</span><span class="nx">missing</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">, </span><span class="dl">'</span><span class="p">)}</span><span class="s2">`</span><span class="p">);</span> <span class="p">}</span> </code></pre></div> <p></p> <h3> <a name="zod-validation-typescript" href="#zod-validation-typescript" class="anchor"> </a> Zod Validation (TypeScript) </h3> <p>Use <a href="https://github.com/colinhacks/zod">Zod</a> for typed, validated environment configuration:<br> </p> <div class="highlight"><pre class="highlight typescript"><code><span class="c1">// env.ts</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">z</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">zod</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">envSchema</span> <span class="o">=</span> <span class="nx">z</span><span class="p">.</span><span class="nf">object</span><span class="p">({</span> <span class="na">DATABASE_URL</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">url</span><span class="p">(),</span> <span class="na">JWT_SECRET</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">min</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span> <span class="dl">'</span><span class="s1">JWT_SECRET must be at least 32 characters</span><span class="dl">'</span><span class="p">),</span> <span class="na">PORT</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nx">coerce</span><span class="p">.</span><span class="nf">number</span><span class="p">().</span><span class="k">default</span><span class="p">(</span><span class="mi">3000</span><span class="p">),</span> <span class="na">NODE_ENV</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">enum</span><span class="p">([</span><span class="dl">'</span><span class="s1">development</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">test</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">]).</span><span class="k">default</span><span class="p">(</span><span class="dl">'</span><span class="s1">development</span><span class="dl">'</span><span class="p">),</span> <span class="na">LOG_LEVEL</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">enum</span><span class="p">([</span><span class="dl">'</span><span class="s1">debug</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">info</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">warn</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">]).</span><span class="k">default</span><span class="p">(</span><span class="dl">'</span><span class="s1">info</span><span class="dl">'</span><span class="p">),</span> <span class="na">STRIPE_SECRET_KEY</span><span class="p">:</span> <span class="nx">z</span><span class="p">.</span><span class="nf">string</span><span class="p">().</span><span class="nf">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">sk_</span><span class="dl">'</span><span class="p">),</span> <span class="p">});</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">env</span> <span class="o">=</span> <span class="nx">envSchema</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">);</span> <span class="c1">// TypeScript now knows the exact types of all env vars</span> </code></pre></div> <p></p> <p></p> <div class="highlight"><pre class="highlight typescript"><code><span class="c1">// app.ts — import from env.ts, not process.env</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">env</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./env</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nf">createServer</span><span class="p">({</span> <span class="na">port</span><span class="p">:</span> <span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="p">});</span> <span class="nx">db</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">);</span> </code></pre></div> <p></p> <h3> <a name="pydantic-settings-python" href="#pydantic-settings-python" class="anchor"> </a> Pydantic Settings (Python) </h3> <p></p> <div class="highlight"><pre class="highlight python"><code><span class="kn">from</span> <span class="n">pydantic_settings</span> <span class="kn">import</span> <span class="n">BaseSettings</span> <span class="kn">from</span> <span class="n">pydantic</span> <span class="kn">import</span> <span class="n">AnyUrl</span><span class="p">,</span> <span class="n">validator</span> <span class="k">class</span> <span class="nc">Settings</span><span class="p">(</span><span class="n">BaseSettings</span><span class="p">):</span> <span class="n">database_url</span><span class="p">:</span> <span class="n">AnyUrl</span> <span class="n">jwt_secret</span><span class="p">:</span> <span class="nb">str</span> <span class="n">port</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">3000</span> <span class="n">debug</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span> <span class="n">stripe_secret_key</span><span class="p">:</span> <span class="nb">str</span> <span class="nd">@validator</span><span class="p">(</span><span class="sh">'</span><span class="s">jwt_secret</span><span class="sh">'</span><span class="p">)</span> <span class="k">def</span> <span class="nf">jwt_secret_long_enough</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">v</span><span class="p">):</span> <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">v</span><span class="p">)</span> <span class="o"><</span> <span class="mi">32</span><span class="p">:</span> <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">'</span><span class="s">JWT secret must be at least 32 characters</span><span class="sh">'</span><span class="p">)</span> <span class="k">return</span> <span class="n">v</span> <span class="k">class</span> <span class="nc">Config</span><span class="p">:</span> <span class="n">env_file</span> <span class="o">=</span> <span class="sh">'</span><span class="s">.env</span><span class="sh">'</span> <span class="n">settings</span> <span class="o">=</span> <span class="nc">Settings</span><span class="p">()</span> <span class="c1"># Raises ValidationError if any required var missing </span></code></pre></div> <p></p> <h2> <a name="docker-and-environment-variables" href="#docker-and-environment-variables" class="anchor"> </a> Docker and Environment Variables </h2> <p>Docker has three ways to pass environment variables:</p> <h3> <a name="1-inline-in-docker-run" href="#1-inline-in-docker-run" class="anchor"> </a> 1. Inline in docker run </h3> <p></p> <div class="highlight"><pre class="highlight docker"><code>docker run -d \ -e DATABASE_URL=postgres://user:pass@db:5432/myapp \ -e NODE_ENV=production \ my-app:latest </code></pre></div> <p></p> <h3> <a name="2-from-a-file" href="#2-from-a-file" class="anchor"> </a> 2. From a file </h3> <p></p> <div class="highlight"><pre class="highlight docker"><code>docker run -d --env-file .env.production my-app:latest </code></pre></div> <p></p> <h3> <a name="3-in-dockercomposeyml" href="#3-in-dockercomposeyml" class="anchor"> </a> 3. In docker-compose.yml </h3> <p></p> <div class="highlight"><pre class="highlight yaml"><code><span class="na">services</span><span class="pi">:</span> <span class="na">app</span><span class="pi">:</span> <span class="na">image</span><span class="pi">:</span> <span class="s">my-app:latest</span> <span class="na">environment</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">NODE_ENV=production</span> <span class="pi">-</span> <span class="s">DATABASE_URL=${DATABASE_URL}</span> <span class="c1"># From shell or .env file</span> <span class="na">env_file</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">.env.production</span> </code></pre></div> <p></p> <p><strong>Important Docker rules:</strong></p> <ul> <li>Never bake secrets into the image with <code>ENV</code> in the Dockerfile — they're visible in <code>docker history</code> and image layers</li> <li>Use Docker secrets for production deployments</li> <li>The <code>.env</code> in docker-compose.yml refers to a file in the same directory as <code>docker-compose.yml</code>, not inside the container</li> </ul> <h2> <a name="cicd-github-actions-and-other-pipelines" href="#cicd-github-actions-and-other-pipelines" class="anchor"> </a> CI/CD: GitHub Actions and Other Pipelines </h2> <h3> <a name="github-actions-secrets" href="#github-actions-secrets" class="anchor"> </a> GitHub Actions Secrets </h3> <p></p> <div class="highlight"><pre class="highlight yaml"><code><span class="s">// .github/workflows/deploy.yml</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">deploy</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">steps</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy</span> <span class="na">env</span><span class="pi">:</span> <span class="na">DATABASE_URL</span><span class="pi">:</span> <span class="s">${{ secrets.DATABASE_URL }}</span> <span class="na">STRIPE_SECRET_KEY</span><span class="pi">:</span> <span class="s">${{ secrets.STRIPE_SECRET_KEY }}</span> <span class="na">run</span><span class="pi">:</span> <span class="s">./deploy.sh</span> </code></pre></div> <p></p> <p>Set secrets in GitHub: <em>Repository Settings → Secrets and variables → Actions → New repository secret</em></p> <p>GitHub automatically masks secret values in logs — if your pipeline accidentally prints <code>$DATABASE_URL</code>, it appears as <code>***</code>.</p> <h3> <a name="repository-vs-environment-secrets" href="#repository-vs-environment-secrets" class="anchor"> </a> Repository vs Environment Secrets </h3> <p>GitHub supports two levels of secrets:</p> <ul> <li><strong>Repository secrets</strong>: Available to all workflows in the repository</li> <li><strong>Environment secrets</strong>: Scoped to specific deployment environments (staging, production). Requires approval workflows for production.</li> </ul> <p>Use environment secrets for production credentials — it prevents a staging deployment from accidentally using production keys.</p> <h2> <a name="production-secrets-management" href="#production-secrets-management" class="anchor"> </a> Production Secrets Management </h2> <p>For production systems, <code>.env</code> files are insufficient. Use a dedicated secrets manager.</p> <h3> <a name="aws-secrets-manager" href="#aws-secrets-manager" class="anchor"> </a> AWS Secrets Manager </h3> <p></p> <div class="highlight"><pre class="highlight shell"><code><span class="c"># Store a secret</span> aws secretsmanager create-secret <span class="se">\</span> <span class="nt">--name</span> myapp/production/database-url <span class="se">\</span> <span class="nt">--secret-string</span> <span class="s2">"postgres://user:pass@prod-db:5432/myapp"</span> <span class="c"># Retrieve at startup (Node.js)</span> import <span class="o">{</span> SecretsManagerClient, GetSecretValueCommand <span class="o">}</span> from <span class="s1">'@aws-sdk/client-secrets-manager'</span><span class="p">;</span> const client <span class="o">=</span> new SecretsManagerClient<span class="o">({</span> region: <span class="s1">'us-east-1'</span> <span class="o">})</span><span class="p">;</span> const response <span class="o">=</span> await client.send<span class="o">(</span>new GetSecretValueCommand<span class="o">({</span> SecretId: <span class="s1">'myapp/production/database-url'</span> <span class="o">}))</span><span class="p">;</span> const dbUrl <span class="o">=</span> response.SecretString<span class="p">;</span> </code></pre></div> <p></p> <h3> <a name="hashicorp-vault" href="#hashicorp-vault" class="anchor"> </a> HashiCorp Vault </h3> <p>Vault is the open-source option for teams that want full control:<br> </p> <div class="highlight"><pre class="highlight shell"><code><span class="c"># Write a secret</span> vault kv put secret/myapp/production <span class="nv">database_url</span><span class="o">=</span><span class="s2">"postgres://..."</span> <span class="nv">jwt_secret</span><span class="o">=</span><span class="s2">"..."</span> <span class="c"># Read it (or use the Agent/dynamic secrets approach)</span> vault kv get <span class="nt">-field</span><span class="o">=</span>database_url secret/myapp/production </code></pre></div> <p></p> <h3> <a name="kubernetes-secrets" href="#kubernetes-secrets" class="anchor"> </a> Kubernetes Secrets </h3> <p></p> <div class="highlight"><pre class="highlight shell"><code>kubectl create secret generic myapp-secrets <span class="se">\</span> <span class="nt">--from-literal</span><span class="o">=</span>database-url<span class="o">=</span><span class="s2">"postgres://user:pass@db:5432/myapp"</span> <span class="se">\</span> <span class="nt">--from-literal</span><span class="o">=</span>jwt-secret<span class="o">=</span><span class="s2">"your-secret"</span> </code></pre></div> <p></p> <p></p> <div class="highlight"><pre class="highlight yaml"><code><span class="c1"># Reference in a Pod spec</span> <span class="na">env</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">DATABASE_URL</span> <span class="na">valueFrom</span><span class="pi">:</span> <span class="na">secretKeyRef</span><span class="pi">:</span> <span class="na">name</span><span class="pi">:</span> <span class="s">myapp-secrets</span> <span class="na">key</span><span class="pi">:</span> <span class="s">database-url</span> </code></pre></div> <p></p> <h3> <a name="simple-cloud-options" href="#simple-cloud-options" class="anchor"> </a> Simple Cloud Options </h3> <ul> <li><strong>Vercel</strong>: Environment variables in the dashboard, scoped per environment (development/preview/production)</li> <li><strong>Railway</strong>: Variables tab in the project, secret values masked in UI</li> <li><strong>Render</strong>: Environment group feature to share variables across services</li> <li><strong>Fly.io</strong>: <code>flyctl secrets set KEY=value</code> — stored encrypted, injected at runtime</li> </ul> <h2> <a name="naming-conventions" href="#naming-conventions" class="anchor"> </a> Naming Conventions </h2> <p>Consistent naming saves time across large codebases and teams:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="c"># All caps, underscore-separated </span><span class="py">DATABASE_URL</span><span class="p">=</span><span class="s">...</span> <span class="py">REDIS_URL</span><span class="p">=</span><span class="s">...</span> <span class="c"># Service prefix for multiple instances </span><span class="py">POSTGRES_PRIMARY_URL</span><span class="p">=</span><span class="s">...</span> <span class="py">POSTGRES_REPLICA_URL</span><span class="p">=</span><span class="s">...</span> <span class="c"># Clear environment indicator </span><span class="py">APP_ENV</span><span class="p">=</span><span class="s">production # or NODE_ENV, RAILS_ENV, DJANGO_ENV</span> <span class="c"># Boolean conventions (use "true"/"false" strings) </span><span class="py">FEATURE_FLAG_NEW_CHECKOUT</span><span class="p">=</span><span class="s">true</span> <span class="py">ENABLE_RATE_LIMITING</span><span class="p">=</span><span class="s">false</span> <span class="c"># Ports are numbers, not URLs </span><span class="py">PORT</span><span class="p">=</span><span class="s">3000</span> <span class="py">METRICS_PORT</span><span class="p">=</span><span class="s">9090</span> <span class="c"># Timeouts in milliseconds or seconds (document the unit) </span><span class="py">DB_CONNECTION_TIMEOUT_MS</span><span class="p">=</span><span class="s">5000</span> <span class="py">REQUEST_TIMEOUT_SECONDS</span><span class="p">=</span><span class="s">30</span> </code></pre></div> <p></p> <h2> <a name="common-patterns-and-pitfalls" href="#common-patterns-and-pitfalls" class="anchor"> </a> Common Patterns and Pitfalls </h2> <h3> <a name="variable-expansion" href="#variable-expansion" class="anchor"> </a> Variable Expansion </h3> <p>Some dotenv implementations support variable expansion:<br> </p> <div class="highlight"><pre class="highlight properties"><code><span class="py">BASE_URL</span><span class="p">=</span><span class="s">https://api.example.com</span> <span class="py">USERS_ENDPOINT</span><span class="p">=</span><span class="s">${BASE_URL}/v1/users # Expands to full URL</span> </code></pre></div> <p></p> <p>Not all implementations support this — check your dotenv library's docs.</p> <h3> <a name="boolean-coercion" href="#boolean-coercion" class="anchor"> </a> Boolean Coercion </h3> <p>Environment variables are always strings. Coercion bugs are common:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="c1">// Bug: "false" is truthy in JavaScript!</span> <span class="k">if </span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DEBUG_MODE</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// This runs even when DEBUG_MODE=false</span> <span class="p">}</span> <span class="c1">// Correct</span> <span class="k">if </span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DEBUG_MODE</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">true</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Only runs when DEBUG_MODE=true</span> <span class="p">}</span> </code></pre></div> <p></p> <h3> <a name="default-values" href="#default-values" class="anchor"> </a> Default Values </h3> <p></p> <div class="highlight"><pre class="highlight shell"><code><span class="c"># Node.js</span> const port <span class="o">=</span> parseInt<span class="o">(</span>process.env.PORT <span class="o">||</span> <span class="s1">'3000'</span><span class="o">)</span><span class="p">;</span> const logLevel <span class="o">=</span> process.env.LOG_LEVEL ?? <span class="s1">'info'</span><span class="p">;</span> <span class="c"># Python</span> port <span class="o">=</span> int<span class="o">(</span>os.getenv<span class="o">(</span><span class="s1">'PORT'</span>, <span class="s1">'3000'</span><span class="o">))</span> log_level <span class="o">=</span> os.getenv<span class="o">(</span><span class="s1">'LOG_LEVEL'</span>, <span class="s1">'info'</span><span class="o">)</span> <span class="c"># Shell</span> <span class="nv">PORT</span><span class="o">=</span><span class="k">${</span><span class="nv">PORT</span><span class="k">:-</span><span class="nv">3000</span><span class="k">}</span> </code></pre></div> <p></p> <h3> <a name="secrets-in-logs" href="#secrets-in-logs" class="anchor"> </a> Secrets in Logs </h3> <p>Never log environment variable values directly, especially in production:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="c1">// Bad</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Config:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">);</span> <span class="c1">// Logs all env vars including secrets</span> <span class="c1">// Good</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Server starting on port</span><span class="dl">'</span><span class="p">,</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span><span class="p">);</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Database:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DATABASE_URL</span><span class="p">?.</span><span class="nf">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">@</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]);</span> <span class="c1">// Log only host, not credentials</span> </code></pre></div> <p></p> <h3> <a name="process-inheritance" href="#process-inheritance" class="anchor"> </a> Process Inheritance </h3> <p>Child processes inherit environment variables from their parent. This can leak secrets to subprocesses unintentionally:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="err">#</span> <span class="nx">Spawn</span> <span class="nx">subprocess</span> <span class="nx">without</span> <span class="nx">inheriting</span> <span class="nx">sensitive</span> <span class="nx">vars</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">execFile</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">child_process</span><span class="dl">'</span><span class="p">);</span> <span class="nf">execFile</span><span class="p">(</span><span class="dl">'</span><span class="s1">some-tool</span><span class="dl">'</span><span class="p">,</span> <span class="p">[],</span> <span class="p">{</span> <span class="na">env</span><span class="p">:</span> <span class="p">{</span> <span class="na">PATH</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PATH</span><span class="p">,</span> <span class="c1">// Only pass what's needed</span> <span class="na">HOME</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">HOME</span><span class="p">,</span> <span class="c1">// Don't include DATABASE_URL, API_KEY, etc.</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div> <p></p> <h2> <a name="tooling-validate-and-manage-env-files" href="#tooling-validate-and-manage-env-files" class="anchor"> </a> Tooling: Validate and Manage .env Files </h2> <h3> <a name="dotenvvault" href="#dotenvvault" class="anchor"> </a> dotenv-vault </h3> <p>Sync <code>.env</code> files across your team encrypted, without sharing them in plaintext:<br> </p> <div class="highlight"><pre class="highlight shell"><code>npm <span class="nb">install</span> <span class="nt">-g</span> dotenv-vault dotenv-vault new dotenv-vault push <span class="c"># Encrypt and push to vault</span> dotenv-vault pull <span class="c"># Pull latest for your environment</span> </code></pre></div> <p></p> <h3> <a name="direnv" href="#direnv" class="anchor"> </a> direnv </h3> <p>Load environment variables automatically when you enter a directory:<br> </p> <div class="highlight"><pre class="highlight shell"><code>brew <span class="nb">install </span>direnv <span class="nb">echo</span> <span class="s1">'eval "$(direnv hook bash)"'</span> <span class="o">>></span> ~/.bashrc <span class="c"># Create .envrc in your project</span> <span class="nb">echo</span> <span class="s1">'dotenv'</span> <span class="o">></span> .envrc direnv allow <span class="nb">.</span> </code></pre></div> <p></p> <p>Now when you <code>cd</code> into the project directory, <code>.env</code> is loaded automatically. When you leave, it's unloaded. No more manually sourcing env files.</p> <h3> <a name="envcmd" href="#envcmd" class="anchor"> </a> env-cmd </h3> <p></p> <div class="highlight"><pre class="highlight json"><code><span class="err">npm</span><span class="w"> </span><span class="err">install</span><span class="w"> </span><span class="err">-D</span><span class="w"> </span><span class="err">env-cmd</span><span class="w"> </span><span class="err">#</span><span class="w"> </span><span class="err">package.json</span><span class="w"> </span><span class="err">scripts</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"env-cmd -f .env node server.js"</span><span class="p">,</span><span class="w"> </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"env-cmd -f .env.test jest"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div> <p></p> <h3> <a name="1password-cli" href="#1password-cli" class="anchor"> </a> 1Password CLI </h3> <p>Load secrets from 1Password at runtime — no local <code>.env</code> file with real credentials needed:<br> </p> <div class="highlight"><pre class="highlight plaintext"><code># .env.local (committed, uses 1Password references) DATABASE_URL=op://MyVault/Production DB/url API_KEY=op://MyVault/Stripe API/secret_key # Run with 1Password injection op run --env-file .env.local -- node server.js </code></pre></div> <p></p> <h2> <a name="quick-reference-checklist" href="#quick-reference-checklist" class="anchor"> </a> Quick Reference Checklist </h2> <table> <thead> <tr> <th>Practice</th> <th>Status</th> </tr> </thead> <tbody> <tr><td>`.env` in `.gitignore`</td><td>Required</td></tr> <tr><td>`.env.example` committed to repo</td><td>Required</td></tr> <tr><td>Startup validation for required vars</td><td>Required</td></tr> <tr><td>No secrets hardcoded in source code</td><td>Required</td></tr> <tr><td>Separate vars per environment (dev/staging/prod)</td><td>Required</td></tr> <tr><td>Secrets manager for production</td><td>Recommended for teams</td></tr> <tr><td>CI/CD pipeline uses secrets management</td><td>Required</td></tr> <tr><td>Boolean coercion handled explicitly</td><td>Required</td></tr> <tr><td>Secrets never logged in plaintext</td><td>Required</td></tr> <tr><td>direnv or equivalent for local workflow</td><td>Nice to have</td></tr> </tbody> </table> <h2> <a name="summary" href="#summary" class="anchor"> </a> Summary </h2> <p>Environment variables done right solve the config/code separation problem permanently. The core practices are simple: use <code>.env</code> files locally, never commit them, validate at startup, and use proper secrets management in production.</p> <p>The investment is small — a <code>.env.example</code>, a startup validator, and a CI/CD secrets setup. The payoff is significant: no credential leaks, no "works on my machine" issues, and a clear contract between environments.</p> <h2> <a name="for-related-tools-try-our-json-formatter-for-inspecting-api-responses-hash-generator-for-generating-secure-secrets-and-base64-encoder-for-encoding-binary-configuration-values-all-tools-are-free-and-work-in-your-browser-with-no-installation" href="#for-related-tools-try-our-json-formatter-for-inspecting-api-responses-hash-generator-for-generating-secure-secrets-and-base64-encoder-for-encoding-binary-configuration-values-all-tools-are-free-and-work-in-your-browser-with-no-installation" class="anchor"> </a> For related tools, try our <a href="https://dev.to/tools/json-formatter">JSON Formatter</a> for inspecting API responses, <a href="https://dev.to/tools/hash-generator">Hash Generator</a> for generating secure secrets, and <a href="https://dev.to/tools/base64-encoder">Base64 Encoder</a> for encoding binary configuration values. All tools are free and work in your browser with no installation. </h2> <h2> <a name="free-developer-tools" href="#free-developer-tools" class="anchor"> </a> Free Developer Tools </h2> <p>If you found this article helpful, check out <a href="https://devtoolkit.cc">DevToolkit</a> — 40+ free browser-based developer tools with no signup required.</p> <p><strong>Popular tools:</strong> <a href="https://devtoolkit.cc/tools/json-formatter">JSON Formatter</a> · <a href="https://devtoolkit.cc/tools/regex-tester">Regex Tester</a> · <a href="https://devtoolkit.cc/tools/jwt-decoder">JWT Decoder</a> · <a href="https://devtoolkit.cc/tools/base64">Base64 Encoder</a></p> <p>🛒 <strong><a href="https://devtoolkit.gumroad.com">Get the DevToolkit Starter Kit on Gumroad</a></strong> — source code, deployment guide, and customization templates.</p>
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)