Case Study 1 — "It Works on My Machine": Standardizing the Dev Database
A look at why the client/server model and reproducible environments matter, set in a small engineering team. The technical lesson is quiet but important: the database is a server you connect to, and which server (and which version) you connect to changes everything.
Background
A six-person team maintained a web application backed by PostgreSQL. Everyone ran the database locally for development. Over a couple of years, through a series of individual laptop setups, the team had quietly drifted into chaos that nobody had decided on:
- Two developers were on PostgreSQL 12 (installed years ago and never upgraded).
- Three were on 15 (the current installer's default when they joined).
- One had 16 via Homebrew and a leftover 13 from a tutorial, with
psqlambiguously pointing at one or the other depending on the day. - The production database was 15.
This is the kind of mess that accretes invisibly. No single decision created it; it was just the sum of six people installing software at six different times.
The symptoms
The trouble showed up as the most demoralizing class of bug: "works on my machine."
- A developer used a SQL feature available in PostgreSQL 15 but not 12. It passed her tests, got merged, and broke the moment a teammate on 12 pulled the branch. An afternoon vanished into "but it works for me."
- A subtle difference in default behavior between major versions caused a query to sort rows differently on different machines. A test that asserted an exact row order passed for some developers and failed for others — seemingly at random, depending on who ran it.
- A new hire spent a day and a half just getting a working database before he could write a line of code. The onboarding doc said "install PostgreSQL," which, it turned out, meant six different things.
None of these were application bugs. They were environment bugs — differences in the server each person was talking to. And because the database is a server you connect to (Chapter 2's mental model), the version and configuration of that server is part of your application's behavior, even though it lives outside your code.
The fix: a database in a box
The team adopted a single, shared definition of the development database using Docker. Instead of "install PostgreSQL," the onboarding instruction became one command, identical for everyone:
docker run --name app-pg \
-e POSTGRES_PASSWORD=devpassword \
-e POSTGRES_DB=appdev \
-p 5432:5432 \
-d postgres:15 # pinned to match production
They committed this to the repository as a docker-compose.yml so it was version-controlled alongside the code, and they pinned the image to postgres:15 to match production exactly. Now:
- Everyone ran the same version, the same one as production. The "works on my machine" version-skew bugs simply stopped happening.
- Onboarding dropped from a day and a half to ten minutes. Clone the repo, run one command, and you had the exact database everyone else had.
- Resetting a corrupted dev database became trivial: destroy the container and recreate it. No more "my local data got weird and I don't know how to fix it."
- Upgrading PostgreSQL became a one-line change to the pinned version, tested by everyone at once, deliberately — instead of drifting per laptop.
The client/server split made this possible. Because the application connects to the database over a port (5432) with a host, database name, user, and password, where the server runs — bare metal, Homebrew, or a Docker container — is invisible to the application. Swap the server for a containerized one, keep the connection details the same, and nothing else has to change.
The analysis
It's tempting to read this as "Docker good." The deeper lesson is about reproducibility and about taking the database seriously as part of your environment:
-
The database is infrastructure, and infrastructure should be reproducible. An environment that exists only as a sequence of manual steps performed long ago, differently, on six machines, is not reproducible — it's archaeology. Whether you use Docker, a setup script, or a managed dev database, the goal is that everyone (and CI, and production) runs a known, identical configuration.
-
Version skew is a real and costly category of bug. Major PostgreSQL versions differ in features and occasionally in default behavior. Pinning the version — and matching production — eliminates a whole family of "random" failures.
-
The client/server model is what makes the swap painless. Because clients connect to a server by address, not by opening a file, you can relocate or replace the server freely. This same property is why your Python app in Part V connects exactly the way
psqldoes, and why production can run a managed database in the cloud (Chapter 38) without your application code knowing or caring.
Discussion questions
- The team pinned their dev database to the same major version as production. Why is matching production specifically valuable? What bugs does it prevent that merely "everyone uses the same version" would not?
- Explain how the client/server model let the team replace each developer's local PostgreSQL with a container without changing the application code.
- A skeptic says "Docker is overkill; just tell everyone to install version 15." What does the one-command, version-controlled approach give you that a written instruction does not?
- ⭐ The new hire's onboarding went from 1.5 days to 10 minutes. Estimate the cost of slow onboarding for a growing team, and argue for or against investing engineering time in reproducible environments.