The "Invisible" Hand: Why My Production Server Was Secretly Living on the Edge

The "Invisible" Hand: Why My Production Server Was Secretly Living on the Edge

I’ve always said that I’m an "accidental" engineer. I build The Hangout Spot because I want the friction removed from my social life, and I self-host it on a $15 VPS because I want to own my data. But this week, I learned that "owning your data" also means owning the mistakes in your deployment scripts.

I almost wiped my production database. And I didn't even know I was doing it.

The Feature: The "Group Order"

The idea was simple: A shared checklist for hangouts. If you're doing a coffee run or ordering pizza, everyone adds their item, and the host checks them off. Technically, this meant adding a few new tables and a boolean flag to the Hangout model in my schema.prisma.

I’d done this a dozen times before. I’d update the schema, push to GitLab, and the app would magically have the new columns. I thought my CI/CD pipeline was just that good.

The Red Flags

When I pushed the Group Order update, my GitLab runner returned a sea of red. ERROR: column "hasGroupOrder" already exists.

I was confused. My pipeline is supposed to run npx prisma migrate deploy—the professional way to move a database forward. If the column already existed, it meant something had skipped the line.

The "Ooff" Moment

I dug into my package.json and found the ghost in the machine. My production start script looked like this:

JSON

"start": "npx prisma generate && npx prisma db push --accept-data-loss && node src/index.js"

The Reality Check: Every single time my Docker container booted up in production, it was running db push with the --accept-data-loss flag.

While I was watching my GitLab logs and feeling proud of my "versioned migrations," my server was actually just forcing the database to match the schema on every reboot. I was "adding" columns successfully, but I was one accidental schema deletion away from db push wiping an entire table to stay "in sync."

The Conflict of Authority

Because db push runs the second the container starts, it was beating my CI pipeline to the punch. By the time GitLab tried to run the official migration script, the database had already been "pushed" into the new shape. The migration saw the changes already there and crashed.

The only reason I hadn't lost data yet? I’ve only been adding features. db push is friendly when you’re giving it more to hold; it’s a monster when you tell it to let go of something.

The Fix: Separation of Concerns

I realized I had to separate my development environment from production one; I didn't want to stop using db push. In Dev, I want speed. In Prod, I want predictability. I stripped the dangerous commands out of the main start script:

Before (The Danger Zone): "start": "npx prisma generate && npx prisma db push --accept-data-loss && node src/index.js"

After (The Safe Path): "start": "npx prisma generate && node src/index.js" "start:dev": "npx prisma generate && npx prisma db push --accept-data-loss && node src/index.js"

Now, db push stays on my local machine. Production only moves when a versioned migration script tells it to.

The Takeaway: Watch the Warnings (And Back Up Everything)

The biggest lesson here wasn't just about package.json scripts; it was about ignoring the smoke before the fire. I had clues. In my testing environment, the "Toast" column would mysteriously vanish and reappear. I wrote it off as a quirk of my local setup. In reality, it was my automation telling me exactly what it was going to do to production eventually.

Database automation is wonderful, but it has to be bulletproof. And when it’s not, you need a safety net that is rock solid.

For me, that safety net is aggressive. I don't just "take backups."

  • Snapshots: My database backs up every 30 minutes locally.
  • Retention: I use a smart rotation policy—keeping hourly backups for a day, dailies for a week, weeklies for a few weeks, and monthlies for a few months.
  • Offsite Sync: Twice a day, an encrypted copy is synced to secure cloud storage.
  • Encryption: This is on top of column-level encryption in the DB and a VPS-managed encrypted drive.

But the real secret weapon? My testing environment is my disaster recovery drill. Every time I deploy to my "Testing/Beta" server, the pipeline automatically grabs a fresh backup from production and sanitizes/restores it. This means I am effectively testing my "Emergency Restore" process multiple times a week. I thankfully didn't need to use my backups to save the Group Order feature this time, but because I use them every day to test, I know they work.

If you’re going to automate your database, make sure your recovery plan is just as automated.