LongCut logo

YouTube Video

By Unknown

Summary

Topics Covered

  • Environment variables are an OS primitive
  • Shell exports silently override .env files
  • Child processes start with copies, not references
  • Frontend env vars ship as plain text in the bundle
  • Secrets management is a spectrum

Full Transcript

This is a React app referencing an environment variable. And this is the

environment variable. And this is the JavaScript file your users actually download. That's your API key in plain

download. That's your API key in plain text shipped to every browser. Or maybe

you've hit this one. Your app runs fine locally, but the moment it's in Docker or CI, it can't find a variable that's right there in your file. We treat

environment variables as copy paste configuration and don't usually think about what's happening underneath. In

this video, we'll see what environment variables actually are at the OS level, work through how they move between processes, which explains most works on my machine problems, and end with a

buildtime versus runtime split.

Before environment variables were a webdev concept, they were an OS concept.

Every process running on your computer, your browser, your terminal, your node server, has its own block of memory called its environment. It's just a flat

list of key value pairs, strings mapped to strings. No nesting, no types, just

to strings. No nesting, no types, just text. Open a terminal and type print

text. Open a terminal and type print env. You're looking at your shell's

env. You're looking at your shell's environment. There are usually dozens of

environment. There are usually dozens of them, and most were set by the OS long before your code entered the picture.

Like path, when you type node, your shell splits this variable at the colons and searches each directory until it finds a match. That's not a node feature. That's your operating system.

feature. That's your operating system.

Environment variables are an OS primitive. Everything else is built on

primitive. Everything else is built on top of that.

So where do all these values get set?

They come from multiple layers and the layers stack. Layer one, the OS sets

layers stack. Layer one, the OS sets baseline variables like home and user when your machine boots. Layer two, your shell profile files like Zshrc that run

when you open a terminal. This is where we add to our path. Layer three, manual commands, export for the whole session or the inline prefix syntax for a single

command. Layer four is application

command. Layer four is application tooling. This is where files come in.

tooling. This is where files come in.

And a file is just a text file. It does

nothing on its own. Something has to read it. In Node, that's traditionally.

read it. In Node, that's traditionally.

Env--end file flag. Python has Python-N.

file flag. Python has Python-N.

Docker Compose has its own loading. Each

tool has its own rules. Most of these tools, including will not overwrite a variable that already exists. If your

shell profile exports database URL and your NV also sets it, your shell profile wins. The Nend file fills in gaps. It

wins. The Nend file fills in gaps. It

doesn't overwrite. So, this can be one of the most common debugging rabbit holes. You're staring at your N, the

holes. You're staring at your N, the values right there, but your app reads something different. an old export in

something different. an old export in your shell profile is silently taking priority. So don't forget to look there.

priority. So don't forget to look there.

And another thing while we're talking about end files, these often contain secrets, API keys, database credentials, so they should always be in yourit ignore. And if you've ever accidentally

ignore. And if you've ever accidentally committed a secret and then deleted it in the next commit, that doesn't fix it.

Git keeps every snapshot. The key is still in your history. Anyone who clones the repo can find it with a git show. So

the rule is simple. Secrets go in environment variables, never in source code. If it's sensitive, it doesn't get

code. If it's sensitive, it doesn't get committed. Okay. So we know what

committed. Okay. So we know what environment variables are, where they come from, and what goes in them. But

how do they actually get from your shell into your application? That's what

process inheritance explains.

Quick question. If you set a variable in your terminal and then launch your app, can your app see it? Yes. If your app changes that variable, can your terminal

see the change? No. And here's why. When

a process creates a child process, the child gets a copy of the parents environment, not a reference, a copy.

They're independent from that point on.

Let's actually see this. I'll set a variable in my shell. Read it from node.

Works fine. Now I'll change it inside node. Confirmed it changed there. Back

node. Confirmed it changed there. Back

in the shell, still hello. the child's

change didn't propagate back up.

Independent copies. This is exactly what happens with Docker. Your shell has dozens of variables loaded from your profile. But when you run a Docker

profile. But when you run a Docker container, it starts with a clean slate.

It doesn't inherit your shell environment. You have to explicitly pass

environment. You have to explicitly pass in everything it needs with - E flags or-n file. And this is why your app

or-n file. And this is why your app works fine on your machine, but breaks in Docker. The container only knows what

in Docker. The container only knows what you explicitly told it. Same thing in CI. Each run step in GitHub actions gets

CI. Each run step in GitHub actions gets a fresh shell. Variables you export in one step don't carry to the next. You

need GitHub_env for that. Different CI systems,

for that. Different CI systems, different mechanisms, same underlying reason. Environments are copies that

reason. Environments are copies that diverge. Knowing that can save you some

diverge. Knowing that can save you some headache. And that React example from

headache. And that React example from the beginning of the video, why the API key ended up in plain text in the browser. Let's look at buildtime versus

browser. Let's look at buildtime versus runtime. Build time configuration values

runtime. Build time configuration values read when your code is compiled or bundled. They get embedded in the

bundled. They get embedded in the output. Once the build is done, they're

output. Once the build is done, they're frozen. Runtime configuration values

frozen. Runtime configuration values read when your app actually starts running. You can change them between

running. You can change them between deployments without rebuilding. Sounds

like a clear distinction, but it gets confusing. In a React app or any

confusing. In a React app or any front-end app, when you write process.env.react_IR,

process.env.react_IR,

Reactapp API URL. It looks like you're reading an environment variable. You're

not. Watch what actually happens. I'll

build a React app, then search the output files for my variable name. And

there it is. The bundler replaced every reference with a literal string. It's

hardcoded into the JavaScript, which means anyone can see it. Open DevTools

in your browser. Click sources. There's

your value in plain text, shipped to every user. If you put a secret in a

every user. If you put a secret in a React app or next public variable, it's not secret. It's in the bundle. And

not secret. It's in the bundle. And

there's another consequence. If you

build your front end with a staging API URL, you cannot take that same build and deploy it to production. The URL is baked in. You have to rebuild. That

baked in. You have to rebuild. That

means the artifact you tested in staging is not the artifact running in production. You rebuilt. So, it's

production. You rebuilt. So, it's

technically a different artifact.

Frameworks try to make this clear.

Next.js JS uses next public for buildtime client variables. Serverside

code reads real environment variables at runtime. Vit uses vit with

runtime. Vit uses vit with import.meta.env.

import.meta.env.

Each framework has its own prefix, but the rule is the same. Prefixed variables

go to the browser. Everything else stays on the server. Compare that to a backend. When Express reads

backend. When Express reads process.env.database

process.env.database

URL, it reads the actual environment of the running process. Change the

variable. Restart. New value, no rebuild needed. That's true runtime

needed. That's true runtime configuration. Rule of thumb, if a value

configuration. Rule of thumb, if a value can't change after the build, a public API URL per deploy pipeline, a feature flag for tree shaking, build time is

fine. Everything else should be runtime.

fine. Everything else should be runtime.

Secrets should always be runtime. Never

build time, never committed to code.

Same idea in Docker. ARG is build time.

It only exists during the image build.

ENV is runtime baked into the image and available to containers. You can copy an ARG to an ENV, but then the value is in the image layer. And both ARG and ENV

values end up visible in Docker history.

ARG values aren't available inside running containers, but they're recorded in the image metadata. So neither of them is safe for secrets. Pass secrets

at runtime with Docker Run-E or your orchestrator secret management. For

secrets, think of it as a spectrum. At

the bare minimum, usev files in your.git

ignore. Better a secret manager like Vault or AWS Secrets Manager that encrypts at rest, logs access, and can rotate credentials automatically. Best

have your infrastructure inject short-lived credentials at runtime. Your

app never stores a static key. Build

time frozen into your artifact. Runtime

flexible until the moment it's read.

Secrets always runtime, never build time, never committed to code. To bring

it all together, environment variables are an operating system primitive, flat strings on every process, not a framework feature. They come from

framework feature. They come from layers OS shell commands tooling and those layers have a precedence order. Child processes get a copy of the

order. Child processes get a copy of the parents environment. After that, they're

parents environment. After that, they're independent. That's why Docker and CI

independent. That's why Docker and CI behave differently from your local terminal. And buildtime configuration is

terminal. And buildtime configuration is not the same as runtime configuration.

One is frozen in your artifact, the other stays flexible. Secrets should

always be runtime. Never build time, never committed to code. None of this is advanced. It's foundational. If this was

advanced. It's foundational. If this was useful, like and share. Thanks for

watching and see you in the next one.

Loading...

Loading video analysis...