You built something. It runs, it works, you can open it in your browser. Then you send the link to a friend and it's http://localhost:3000, and on their phone it does nothing at all. This page explains why, and what actually happens between "it works on my laptop" and "anyone in the world can use it". That journey is called deploying, and once you see the steps, it stops being mysterious.

localhost: the computer you're sitting at

When you start a web app on your own machine, the terminal usually says something like Listening on http://localhost:3000. Two pieces of that address matter:

So when your friend types localhost:3000 on their phone, their phone dutifully looks at itself, finds nothing on door 3000, and gives up. "localhost" means a different machine for every person who types it.

A server is a computer that never goes home

To let other people use your app, it has to run on a computer that is (a) switched on all the time, (b) connected to the internet, and (c) reachable by an address everyone can find. That computer is called a server. Physically it's nothing magic: usually a rented machine (or a slice of one) in a data centre, run by a hosting company. "The cloud" mostly means "somebody else's computers that you rent".

💻 Your laptop

  • Address: localhost:3000
  • Only you can reach it
  • Off when the lid closes
  • Fake test data, fine to break

🖥️ A server

  • Address: https://shop.example.com
  • Anyone on the internet can reach it
  • On 24/7, restarts itself if it crashes
  • Real users, real data: don't break it
Same code, two very different homes.

A server on the internet has a public IP address, a number like 203.0.113.10. Nobody wants to type that, so you buy a domain name (like example.com) and use DNS, the internet's phone book, to say "shop.example.com lives at 203.0.113.10". That entry is called an A record. You set it up once; after that, every deploy just replaces the code running at that address.

Development, staging, production

Professional teams don't run their code in just two places. They run it in several copies called environments. Each is the same app, set up for a different job:

EnvironmentWhereWho uses itPurpose
developmentyour laptopyouwrite code, try things, break things
staginga serverthe team, testersa dress rehearsal that looks like the real thing
productiona serverreal usersthe real thing ("prod")

Staging exists because some problems only show up on a real server: a missing setting, a slower database, a different operating system. Catching them on staging means a tester sees the bug, not a customer. Small personal projects often skip staging and go straight from laptop to production, and that's fine while you're learning.

Here's the important part: the code is identical in every environment. What differs is the configuration: which database to connect to, which port to listen on, whether to charge real money. So how does the same code know which environment it's in?

Same code, different settings: environment variables

The usual answer is environment variables. They're named values (like PORT=8080) that the operating system hands to a program when it starts. The program reads them instead of having the values typed into the code. In JavaScript running on Node.js they live in process.env; in Python it's os.environ. Below is a tiny app that reads its settings that way. Switch between environments. Watch which part changes and which part doesn't.

Same code, three environments

Settings (differ)

app.js (never changes)

const env  = process.env.APP_ENV || "development";
const port = process.env.PORT || 3000;
const db   = process.env.DATABASE_URL;
const pay  = process.env.PAYMENTS_KEY || "";

if (!db) {
  throw new Error("DATABASE_URL is not set");
}

console.log(`[${env}] listening on port ${port}`);
console.log(`database: ${new URL(db).host}`);
console.log(`payments: ${pay.startsWith("sk_live_")
  ? "LIVE money" : "test mode"}`);

Output when it starts

Notice what the code doesn't contain: no passwords, no server names, no keys. It only says "give me whatever DATABASE_URL is set to". On your laptop that's a throwaway local database. In production it's the real one. The || 3000 part is a default: if PORT isn't set, use 3000. And the if (!db) check makes the app stop immediately with a clear message when an essential setting is missing, which is much better than half-starting and failing later in a confusing way.

On your laptop, typing all those variables every time would be painful, so most projects keep them in a file called .env in the project folder, and a small library (or, in recent Node versions, the --env-file=.env option) loads it at startup. On a server you type the values into your hosting provider's settings page instead, and it passes them to the app when it starts.

The golden rule: never commit secrets

A secret is any value that gives access to something: a database password, an API key, a payment key. Secrets must never go into your code or into git, the tool that records every version of your project. The usual way to guarantee that is to list .env in a file called .gitignore, which tells git to pretend that file doesn't exist:

# .gitignore
.env
node_modules/

Then commit a harmless .env.example with the variable names and blank or fake values, so the next developer knows what to fill in.

Why so strict? Bots scan public GitHub repositories around the clock looking for keys, and leaked cloud keys have been abused within minutes of being pushed. And git remembers everything: deleting the file in your next commit does not remove it from the history. If a secret ever leaks, the fix is to revoke it and create a new one (called rotating the key), not just to delete the line.

The deploy itself: push, build, test, run

"Deploying" means getting a new version of your code from your laptop onto the server and switching users over to it. In the early days people did this by hand, copying files over. Today most projects use a pipeline: a series of automatic steps that runs every time you push code. You'll hear it called CI/CD (continuous integration / continuous deployment), and tools like GitHub Actions, GitLab CI or your hosting provider run it for you.

A typical pipeline looks like this:

  1. Commit: save a snapshot of your change in git on your laptop (git commit).
  2. Push: send it to the shared copy on GitHub or similar (git push origin main). This is what kicks off everything else.
  3. Build: on a fresh machine, download your code, install its dependencies (other people's code it relies on) and prepare it to run.
  4. Test: run the automated tests. Any failure stops the line.
  5. Deploy: start the new version on the server, with production's environment variables, and check it's healthy.
  6. Live: switch traffic for your domain to the new version.

The key idea is that each step is a gate. If any gate fails, the pipeline stops, and the version that's already live stays live. Your users never see the broken one. Try it: run a clean deploy first, then pick a mistake and run it again.

Deploy pipeline · shipping v2

Break something (optional)

shop.example.com is servingv1

Each mistake was caught at a different gate. The typo was caught when the code was first checked. The wrong calculation passed the build (it's valid JavaScript, just wrong) and was caught by a test. The missing setting got through both, because tests run with test settings, and only showed up when the new version tried to start on the real server. That's why the deploy step checks the app is actually running before sending users to it, and why staging, with settings that mirror production, is worth having.

And if something bad does slip through? Every version is still in git, so you can roll back: deploy the previous version again. Most hosting platforms have a one-click button for exactly that.

Check yourself

You're running your app and send your friend the link http://localhost:3000. They open it on their phone. What happens?

localhost always means "this computer", so the phone looks for a program on its own port 3000. Even on the same Wi-Fi, the name localhost still points at the phone itself.

You accidentally commit a .env file with a real API key, then delete it in the next commit. Are you safe?

Git keeps every earlier version, so the key is still in the history, and bots may have copied it within minutes. Treat a leaked secret as burned: rotate it.

Your pipeline is shipping v8 and the test step fails. What do users of the live site see?

A failed gate stops the pipeline before the deploy step, so the live version is never touched.

Where should your production database password live?

The code reads process.env.DATABASE_URL; the actual value is set on the server (in your host's settings) and never stored in the code or the git history.

The short version

Next time someone says "it works on my machine", you'll know exactly what's still missing: a server, the right settings, and a pipeline to get it there safely.