Make pnpm dev Bring Up Your Whole Stack in One Command
Wire a predev hook so pnpm dev brings up Docker, runs migrations, and checks env automatically with no manual prerequisites.
Here is a small friction that quietly costs teams hours. A new engineer clones the repo, runs pnpm dev, and the app crashes because MySQL is not running. They dig through a stale README, find docker compose up -d, run it, try again, and now migrations have not applied. Half an hour later they are finally looking at the app. Multiply that by every new hire, every machine reset, and every time someone pulls a branch that added a Redis dependency, and you have a recurring tax on focus that nobody put on the books.
The fix is a standard we hold on every project: one command brings up the whole stack. pnpm dev should boot the database, run migrations, check the environment, and start the app, in that order, every time, with no manual prerequisite steps. The engineer should never have to remember to run anything before dev. The script remembers for them.
The shape of the problem
A modern app does not run in isolation. It needs a database, often a cache, sometimes a message broker, plus a set of environment variables that have to exist before the app will boot. In production all of that is handled by the deploy pipeline. In development it gets handled by a wiki page that drifts out of date the moment someone adds a dependency, which means the wiki is wrong exactly when it matters most, on the day the stack changed.
The reliable place to put this logic is a lifecycle hook that runs automatically before the dev server starts. With npm-style scripts, a script named predev runs immediately before dev whenever you invoke npm run dev. That gives you a guaranteed slot to bootstrap dependencies before the app touches them.
Wire the predev hook
The core pattern is two scripts. predev brings up infrastructure and prepares the database. dev starts the app. Because predev runs first, the app never races against an unready service.
{
"scripts": {
"predev": "node scripts/bootstrap.mjs",
"dev": "next dev -p 3040"
}
}
The bootstrap script does the unglamorous work: bring up Docker containers and wait until they are actually healthy, verify the environment file exists and has the variables the app needs (the same hot-reload runtime where a stray DB pool can exhaust MySQL connections if you are not careful), then run migrations. Keeping it in a small Node script rather than a chain of shell && keeps it readable, cross-platform, and easy to extend when the next dependency arrives.
// scripts/bootstrap.mjs
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
if (!existsSync(".env.local")) {
console.error("Missing .env.local. Copy .env.example and fill it in.");
process.exit(1);
}
execSync("docker compose up -d --wait", { stdio: "inherit" });
execSync("pnpm migrate", { stdio: "inherit" });
That single bootstrap is the difference between a project a new engineer is productive in within a minute and one that eats their first morning.
Wait for healthy, do not just start containers
The subtle bug here is timing. docker compose up -d returns the instant the containers are created, not when the services inside them are ready to answer. MySQL can take several seconds to finish initializing. If the app boots in that window, the first query fails with a connection refused and the dev server crashes before the database is awake.
The fix is docker compose up -d --wait, which blocks until every service with a healthcheck reports healthy. For that to mean anything, each service needs a real healthcheck in the compose file, not a guess about how long startup takes.
services:
db:
image: mysql:8
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 3s
timeout: 5s
retries: 10
Now up --wait actually waits for MySQL to answer a ping, not for an arbitrary sleep to expire. The app starts only when the database can serve queries, and the startup race disappears.
The pnpm caveat worth knowing
If your team uses pnpm, there is one gotcha. By default, recent pnpm versions do not run npm-style pre/post lifecycle hooks the way npm and yarn do, so a bare pnpm dev may skip predev entirely. The fix is to enable lifecycle scripts in your pnpm configuration, or to call the bootstrap explicitly inside the dev script so it runs regardless of which package manager fired it. Verify the hook actually executes on a clean machine before you trust it, because a bootstrap that silently does not run is worse than no bootstrap at all. It gives the team false confidence right up until the database is missing.
The same idea covers PM2 and production parity
The principle is not specific to local dev. Anywhere a process manager starts your app, the dependency bootstrap belongs in front of it. For a PM2-managed deployment, the bootstrap lives in a wrapper script that PM2 launches, or in a pre-start step in the ecosystem config, so the server brings up what it needs before the app process comes alive, the same discipline behind a zero-downtime PM2 deploy. That same care about process ownership is what avoids the PM2 multi-daemon trap that breaks your next deploy. The single command that starts the app is also the command that makes the app's world ready.
This matters because production parity is where most "works on my machine" bugs are born. If local dev quietly depends on a database someone started by hand three days ago, the moment that container is gone the illusion breaks, and the breakage looks like a code bug when it is really an environment one. A bootstrap that runs every time makes the environment reproducible, which makes failures honest. When the app breaks, it is the code, because the environment is guaranteed.
Why this is a baseline, not a nicety
We treat one-command bootstrap as a default on every project, the same way we treat parameterized queries or security headers. It is the same automate-the-environment instinct that makes the scripts the single source of truth so nobody fixes production by hand. It is not a convenience feature you add when you have spare time. It is the line between a codebase that respects the next person's morning and one that taxes it. A new engineer cloning the repo and being productive in sixty seconds is a quiet signal that the whole project was built by people who think about the seams, and that signal compounds across every onboarding and every machine reset for the life of the product.
The deeper habit underneath it is that the right thing should happen by default, not by discipline. Discipline fails: people forget, READMEs rot, the one person who knew the manual steps leaves. Automation does not forget. When we build a web application we wire this in from the first commit, so the stack comes up clean on any machine, and we carry the same automate-the-environment principle into how we run servers in production. The command that starts the app should start everything the app needs. Anything less is a tax you keep paying.






