Module 4 · Working on real code

4.4 What deployment actually means: environments, builds, and configuration

Lesson 4.4 · 11 min read

Right now, the thing you're building runs on your laptop. You start it, you visit an address that only exists on your machine, and it works. To everyone else on Earth, it does not exist.

Deployment is the act of changing that. It's how the code on your computer becomes something running on a server somewhere that anyone can reach. Most people learn deployment as a sequence of clicks that mysteriously produces a URL, which works fine until it doesn't, and then they're stranded, because they never understood what was actually happening.

So let's understand it. Not as a procedure to follow, but as three ideas that explain everything: your code has to run somewhere, it has to be prepared before it runs, and it has to be configured differently depending on where it's running. Those three ideas are environments, builds, and configuration, and together they explain nearly every deployment problem you will ever have.


Localhost is a real server, which is the whole trick

Start by dissolving something that feels magical.

When you run your project locally and visit localhost:3000, you are not doing something fundamentally different from visiting a real website. Your machine is running a web server. It's listening for requests. Your browser sends one, exactly as described in the very first post, and the server responds. The entire client-server journey is happening, just with both computers being the same computer.

localhost simply means "this machine." That's why nobody else can reach it: the address literally refers to the computer you're sitting at. And 3000 is a port, a numbered door on that machine, because one computer can run many servers at once and they need to be told apart. Your frontend on port 3000, your backend on 8000, your database on 5432.

So deployment isn't teaching your code to be a web server. It already is one. Deployment is putting that same server on a machine that has a public address, is always on, and is reachable from anywhere. The difference between localhost:3000 and a real URL is not a difference in kind. It's a difference in where the machine is and who can reach it.

Once that lands, deployment stops being mystical. You have a program that serves requests. You need it running on a computer that isn't yours, that never sleeps, with an address people can find.


Environments: the same code, in different worlds

Here's a problem you can feel immediately. You want to try a risky change. Where do you try it?

Not on the thing your users are using. Obviously. But also not only on your laptop, because your laptop is not the real world. Your laptop has a tiny database with three fake users, no real traffic, and a slightly different operating system. Something that works there can fail spectacularly on a real server. So you need somewhere in between: a place that resembles the real thing closely, that you can break freely.

That is what environments are. The same codebase, running in different places, for different purposes, with different data.

Development is your machine. Fake data, instant feedback, everything breakable. You work here.

Staging is a real server, configured to be as close to production as possible, but with data nobody cares about and no real users. This is where you find the problems that only appear outside your laptop. It is a rehearsal on the actual stage, with the actual lights, before the audience arrives.

Production is the real thing. Real users, real data, real consequences. Every change that reaches here has, ideally, passed through the others first.

Some teams have more, some fewer, and the names vary. What doesn't vary is the principle: you need somewhere realistic to be wrong before you're wrong in front of your users. Skipping staging is a decision to discover your problems in production, which is a decision to discover them along with your users, which is a decision you should make consciously if you make it at all.

The critical rule that people violate constantly, usually by accident: environments must not share a database. Your staging environment pointing at your production database means a test that deletes some records has just deleted real customer data. This sounds like something nobody would do, and it happens all the time, because the configuration was copied from one place to another and nobody looked carefully at what it pointed to. Which brings us, not coincidentally, to configuration.


Configuration: the same code, behaving differently

Here is the idea that makes environments possible, and it's the one that trips people up most in practice.

Your code must do different things in different environments. Locally, it should talk to your local database. In production, it must talk to the production database. Locally, it might use a payments service in test mode where nothing is real. In production, it must use the live one, where money genuinely moves.

But it's the same code. You do not maintain two versions. That would be a nightmare, and the whole point of staging is that it runs the same code as production.

So the values that differ between environments must live outside the code. The code says "connect to the database at whatever address I've been given." The environment supplies the address. That's it. Those supplied values are environment variables, and they are how one codebase behaves correctly in many different worlds.

This solves two entirely separate problems at once, which is why it's such an important pattern.

The first is the one we just described: different environments need different values. Same code, different configuration, correct behavior everywhere.

The second is secrecy, and it connects directly to everything the security posts told you. Your database password, your API keys, your service credentials: none of these can live in your code, because code goes into a repository, and a repository is shared, and a secret in a repository is a secret that has leaked, permanently, exactly as the Git post explained. Environment variables give secrets a place to live that isn't your code. They sit on the server, supplied to the program at the moment it starts, never written down anywhere Git can see.

Practically: your machine has a .env file holding your local values. That file is listed in .gitignore and never enters the repository. The repository instead contains .env.example, which lists the names of every variable needed, with no values, so anyone setting up the project knows what to supply. And your hosting platform has a settings panel where you enter the production values, which it injects into your program when it runs.

There is one wrinkle that catches nearly everyone, and it's worth understanding rather than memorizing. Environment variables in a frontend are not secret. They get compiled into the code that ships to the browser. Anyone can read them. This is why frontend frameworks require a special prefix on any variable meant for the browser, as a deliberate speed bump that makes you think about what you're exposing. A variable without the prefix stays on the server. A variable with it goes to the user's machine, where it is public. If you find yourself putting an API key behind that prefix to make an error go away, stop. You are about to publish a secret, and it will look like it worked.

Interactive: environments and config

A hands-on visual for this idea is in the works. The text around it carries everything you need for now.


The build: preparing code to run

The last piece. The code you write is not the code that runs.

What you write is optimized for humans. It's spread across hundreds of files, written in modern syntax, full of helpful comments and readable names, with type annotations and JSX and imports of packages you didn't write. Browsers don't understand most of that, and even where they do, sending it as-is would be slow and wasteful.

So before your code runs anywhere real, it goes through a build: an automated transformation that turns human-friendly source code into machine-friendly output.

Several things happen there, and knowing what they are makes build errors comprehensible. Modern syntax is translated into something older browsers understand. Files that import each other are bundled together into a few files instead of hundreds, so the browser makes fewer requests, which you'll recognize as the latency lesson from post 0.3 applied at the code level. Names get shortened, comments and whitespace get stripped, unused code gets removed, all to make the download smaller. And if you're using anything that isn't plain JavaScript, TypeScript, JSX, styling languages, it gets compiled down into what a browser actually runs.

The output is a folder of files, typically small, unreadable to humans, and ready to be served. That folder is what actually gets deployed. Your source code, the thing in your repository, never runs in production. Its descendant does.

Two useful consequences fall out of this.

First, the build is where a whole class of errors surfaces, and they surface before your users see anything. If you referenced a variable that doesn't exist, or imported something you forgot to install, or wrote something the compiler rejects, the build fails. Nothing gets deployed. The site keeps running the previous version. A failed build is not a disaster, it's the system doing exactly its job, which is catching the problem at the last safe moment. When a deploy fails and you feel a spike of alarm, remember: nothing broke. Something was prevented from breaking.

Second, the build needs its own environment variables, and this is where a genuinely confusing bug lives. Some variables are needed while the code is being built, others when it's running afterward. A variable that exists at runtime but was missing during the build produces code that was compiled with a hole in it, and no error appears until a user hits that path. If something works locally and mysteriously not in production, "did that variable exist at build time" is a question worth asking early.


Putting it together: what happens when you deploy

Now the whole picture, and you'll notice you already understand every step.

You push your branch, and it's merged into main. Your hosting platform is watching that repository, and a merge into main is a signal. It begins.

It fetches the code, that exact snapshot, by its commit hash. It installs the dependencies, the packages your code imports and doesn't contain. It runs the build, transforming your source into the optimized output, using whatever build-time environment variables it's been given. If anything fails here, it stops, tells you, and the currently-running version continues serving users, untouched.

If the build succeeds, the output is placed onto servers, and the runtime environment variables are supplied to the running program. The platform then switches incoming traffic to point at the new version. Modern platforms do this atomically: at no moment are half your users on the old version and half on the new. There's a moment before, and a moment after.

And because the previous version still exists, sitting there, complete, the platform can point traffic back at it instantly. That's a rollback, and it is the most important safety property in deployment. When you ship something broken, you do not need to find the bug, fix it, and deploy a fix while users suffer. You point at the last known good version, in seconds, and then you go debug in peace. Knowing rollback exists changes how it feels to ship, and knowing where the button is before you need it is worth five minutes today.


Why understanding this matters when the platform does it for you

Modern platforms have made all of this astonishingly easy. Connect a repository, and every merge to main is built and deployed automatically. You may never run a deploy command in your life.

Which is exactly why understanding it matters, in precisely the way the last post argued about Git. The platform's convenience is real, and it evaporates the moment something is wrong, because now you're reading logs about a process you never understood.

When a deploy fails, you now know it happened at one of three stages: installing dependencies, building, or running. Each has a completely different cause and a different fix. When it works locally and fails deployed, you know the candidates: a missing environment variable, something that existed on your machine but was never declared as a dependency, a value that differed between build time and runtime. When you see a .env file about to be committed, you know exactly what that would mean. When someone says "just point staging at the production database for now," you know precisely what they're proposing, and you say no.

And when your users are looking at a broken site, you know that the fastest correct action is almost never to debug. It's to roll back, restore the working version, and take the pressure off. That single instinct, learned before you need it, is worth the entire post.

You have a program that serves requests. It needs to run on a machine that isn't yours. It has to be prepared before it runs, and configured differently depending on where it's running, and its secrets must never travel with its code. Everything else in deployment is a detail hanging on those sentences.


Do this before the next post

Go find the .env situation in whatever you're building, because this is the one that bites hardest and is easiest to check.

Look for a .env file. Open .gitignore and confirm it's listed there. Then run git status and make sure the .env file does not appear as something waiting to be committed. If it appears, stop and fix that before doing anything else. If you have already committed one at some point in the past, understand clearly what that means: the secret is in your history permanently and must be rotated, not deleted. Go rotate it.

Then look at your environment variables and sort them into two piles. Which of these are secrets, things that must never reach a browser? And which are merely configuration, an API address, a feature flag, something harmless? If your frontend framework uses a prefix to mark variables as public, check that nothing in the first pile has that prefix. That check takes one minute and it is the single most valuable minute in this post.

Finally, if you have anything deployed, go and find the deploy logs on your hosting platform, and read one, even a successful one. Watch it fetch the code, install the dependencies, run the build, and go live. You will see the three stages laid out in order, exactly as described here. Then find the rollback button, and note where it is. You are not going to need it today. You will need it eventually, and the day you do is a bad day to be discovering where it lives.