infrastructure/environment-variables-how-the-same-code-talks-to-a-practice-database-o.md

Environment variables: how the same code talks to a practice database on your laptop and the real one online

Infrastructure · 5 min read ·

The same program can talk to a practice database on your laptop and the real one on a live website, without a single edit to its code. The trick is the *environment variable*: a named piece of text the computer hands to a program when it starts.

Try it on two pretend computers, untick a variable and watch the program stop with a clear error. Along the way you will meet the "False" is true trap, the choice between a default and a loud failure, and why secrets stay out of Git.

Two boxes, a laptop and a production server, each hand a different DATABASE_URL to the same app.py

You write a program that talks to a database. On your laptop it should use a practice database full of fake customers. On the real website it must use the real database. The code is the same in both places. So how does the program know which database to use, without you editing the code every time you move it?

The answer is a small, old idea called an environment variable.

A setting that lives outside your code

An environment variable is a named piece of text that the computer hands to a program when the program starts. It has a name and a value, like DATABASE_URL and postgres://localhost/shop_dev. Every program that runs gets its own copy of the variables, and it can read them but never needs to know where they came from.

In a Mac or Linux terminal you can set a variable for one run by putting it in front of the command:

DATABASE_URL=postgres://localhost/shop_dev python app.py

(In Windows PowerShell the same idea is written $env:DATABASE_URL = "postgres://localhost/shop_dev" on its own line before the command.) A hosting service has a settings page where you type the same names and values for your live site. The code never changes. Only the variables around it do.

Reading them in Python

Here is a tiny program. It reads three variables and prints what it would do. The fake addresses use example.com, a name reserved for examples, so nothing here points at a real server.

import os
from urllib.parse import urlparse

url = os.environ["DATABASE_URL"]
port = int(os.environ.get("PORT", "8000"))
debug = os.environ.get("DEBUG") == "1"

db = urlparse(url)
print("Database:", db.path.lstrip("/"), "on", db.hostname)
print("Listening on port", port)
print("Debug pages:", "ON" if debug else "off")

os.environ works like a dictionary of text. The line with square brackets, os.environ["DATABASE_URL"], demands the variable: if it is missing, Python stops with an error. The .get("PORT", "8000") version offers a default: if PORT is missing, use "8000". Which one to use is a real decision, and we will come back to it.

Try it: same code, two machines

Below is that exact program running on two pretend computers. Each has its own variables. Untick a variable to remove it, or type a new value, and watch what the program does.

Three things are worth noticing. The laptop talks to shop_dev and the server talks to shop, from identical code. Removing DATABASE_URL stops the program at once, with a clear message naming the missing variable. And PORT being unset is fine, because the code has a default.

Why not write it in the code?

You could type the address straight into app.py. There are three problems with that. First, you would need to edit the code to move it between machines, and it is easy to forget and point your practice run at real data. Second, your code is usually copied everywhere: into Git, to teammates, to hosting services. Third, and most serious, a setting like a database password would be saved in Git's history, and deleting the line later does not remove it from older saves. Keeping settings outside the code solves all three.

Three habits that save beginners

1. Values are always text

An environment variable holds text, never a number or a true/false. So PORT arrives as "8080" and has to be turned into a number with int(...). The sneaky case is true/false. Here is a real run:

import os

os.environ["DEBUG"] = "False"          # pretend the setting is "False"
print(repr(os.environ["DEBUG"]))       # values are always text
print(bool(os.environ["DEBUG"]))       # any non-empty text is truthy
print(os.environ["DEBUG"] == "1")      # compare to an exact word instead

Output:

'False'
True
False

The word "False" is still text, and in Python any non-empty text counts as true. So bool("False") is True, the opposite of what you meant. Comparing to one exact word, like == "1", avoids the trap.

2. Decide: crash early, or fall back?

import os

print(os.environ.get("PORT", "8000"))   # missing: use the default
try:
    os.environ["DATABASE_URL"]          # missing: no default
except KeyError as e:
    print("KeyError:", e)

Run without either variable set, it prints:

8000
KeyError: 'DATABASE_URL'

A harmless setting such as a port can have a default. Something where a wrong guess is dangerous, like which database to connect to, is better as a loud failure at startup than a quiet guess. Imagine a default that points at the real database, used by accident on someone's laptop.

3. Keep secrets out of the code and out of Git

Passwords and keys belong in variables, not in saved code. For local work, many people put them in a plain text file named .env with one NAME=value per line, and add .env to a file called .gitignore, which tells Git to never save it. Python needs a small helper library to load such a file (python-dotenv is a common one). Node.js can load one itself: since version 20.6 you can run node --env-file=.env app.js.

One more fact that surprises people: a program gets its variables when it starts. If you change a variable afterwards, a program that is already running will not notice. Restart it.

What to take away

Code says what to do. Environment variables say where and with which keys. Read each one, remember it is text, choose a default only when a wrong guess is harmless, and keep anything secret out of Git. The next time a tutorial says "set your environment variables", you will know it means: tell the program about the world around it before it starts.

$ ls infrastructure/

see all →
infrastructure/exponential-backoff-why-waiting-longer-between-retries-keeps-a-struggl.md Exponential backoff: why waiting longer between retries keeps a struggling server alive When a server stumbles, every client that failed retries at the same instant, and the second wave can be bigger than the first. Exponential… 5 min read · 27 Oct infrastructure/percentiles-why-an-average-response-time-of-235-ms-can-hide-users-wait.md Percentiles: why an average response time of 235 ms can hide users waiting 3 seconds Your dashboard says the average response time is 235 milliseconds, yet some users wait three seconds. Both can be true, because an average… 4 min read · 21 Oct infrastructure/ci-pipelines-how-a-computer-decides-a-change-is-safe-to-merge.md CI pipelines: how a computer decides a change is safe to merge Push a change and a green tick or a red cross appears a minute later. How does a machine decide? It does not understand your code. It reads… 5 min read · 18 Oct infrastructure/rolling-deploys-how-a-website-updates-without-going-offline.md Rolling deploys: how a website updates without going offline How does a busy website switch to a new version without going offline? Not by replacing everything at once. A rolling deploy updates one… 5 min read · 15 Oct infrastructure/cron-schedules-five-fields-that-tell-a-computer-when-to-run-a-job.md Cron schedules: five fields that tell a computer when to run a job Want a computer to run a job at 3 a.m. while you sleep? On Linux and macOS, a program called cron does it, and each job is described by… 5 min read · 12 Oct infrastructure/docker-build-cache-why-a-one-line-change-can-trigger-a-full-reinstall.md Docker build cache: why a one-line change can trigger a full reinstall You change one line of code, rebuild your Docker image, and wait while every library downloads again. Often the cause is just the order of… 4 min read · 8 Oct