I Almost Deleted the Production Database While Setting Up Staging
One Command From Disaster
I was setting up a staging environment for an app that was already live. To keep things simple — and cheap — staging went on the same server as production: same machine, same PostgreSQL instance, two databases sitting side by side.
I'd created the staging database, wired up its .env, and was about to reset it to a clean slate. My finger was on the enter key for a destructive command... pointed at the production database. Same SSH session, same psql, near-identical database names. I caught it with about a second to spare.
Nothing was lost. But the gap between "fine" and "career-defining incident" was a single character in a database name. That scare is worth more to me than the convenience that caused it — so here's what went wrong and the rules I now refuse to skip.
Why Co-Hosting Staging and Production Is a Trap
It feels harmless. One server is cheaper, and "I'll just be careful" sounds like a plan. It isn't, because every safety margin quietly disappears:
- The databases are one typo apart.
app_productionandapp_stagingin the samepsqlsession. Muscle memory and a copy-pasted command don't care which one you meant. - One connection can reach both. If your shell can talk to staging, it can usually talk to prod too. There's no wall — just your attention.
.envconfusion. Two environments, two env files, one working directory. Run the right command against the wrong config and it's instant.- Destructive commands are designed to wipe.
migrate:fresh,db:wipe,DROP DATABASE— their whole job is to destroy data. They do it well, and they don't ask twice once you add--force.
The Guardrails I Now Use
The goal isn't "be more careful." Careful fails at 2am. The goal is to make the dangerous thing hard to do by accident.
1. Isolate, don't just separate
The real fix is separate servers — or at minimum separate containers, separate PostgreSQL clusters on different ports, and separate database users. Give the staging user no privileges at all on the production database. Then a staging connection physically cannot touch prod, no matter what you type. Least privilege turns a catastrophe into a permission error.
2. Make the database names impossible to confuse
Not app and app2. Use names that shout which is which — app_production vs app_staging — so a destructive command against the wrong one looks obviously wrong before you run it.
3. Let Laravel refuse destructive commands in production
This is the single best line I've added. In a service provider's boot():
use Illuminate\Support\Facades\DB;
public function boot(): void
{
// Blocks db:wipe, migrate:fresh, migrate:refresh, migrate:reset
// whenever the app is running in production.
DB::prohibitDestructiveCommands($this->app->isProduction());
}
Now the framework itself slams the door on the exact commands that nearly got me — in production, they simply won't run.
4. Respect the confirmation prompt — don't reflexively --force
Artisan already warns you with "Application In Production!" before destructive migrations. That prompt exists precisely for this moment. The habit of pasting --force to skip it is how the safety net gets cut. On prod, type it out and read it.
5. Colour your production shell red
Visual cues work when attention doesn't. I make production sessions unmistakable. For psql, in ~/.psqlrc on the prod box:
\set PROMPT1 '%[%033[1;31m%]PROD %/%[%033[0m%]=> '
A bright red PROD prompt is a gut-check every time I run anything. Do the same for the bash prompt (PS1) so even cd-ing around the server reminds you where you are.
6. Back up before anything destructive
Even with every guard above, take a dump first:
pg_dump -U app -d app_production -Fc -f /backups/pre-change-$(date +%F-%H%M).dump
Combined with point-in-time recovery, this is the difference between "we restored in five minutes" and "we lost a day of customer data."
The Takeaway
I didn't lose anything that night, and the only reason was luck — I happened to glance at the database name. Luck is not a strategy. Every rule above exists to remove luck from the equation: separate users so the connection can't reach prod, framework guards so the command won't run, red prompts so I always know where I am, and backups so a mistake is recoverable instead of final.
If your only protection against deleting production is your own attention, you don't have protection — you have a countdown.