← All posts jacob@stephens.page
Notes · DevOps

The Boring Deploy Script

June 3, 2026 · ~560 words · Single-server PHP deployment

For a single-server PHP application, I deploy with a shell script - about 200 lines of bash I can read in one sitting. Not a hosted CI/CD pipeline, not a container orchestrator. People sometimes read that as "hasn't gotten around to setting up real CI." It's the opposite: a deliberate choice. This note is about why you'd choose the boring thing on purpose.

What the script actually does

The job is small and specific: get the right commit onto this one box, atomically, without two deploys colliding or a bad ref going live. So the script is mostly guardrails:

Tests and static checks run separately, in hosted CI on push. The split is deliberate: CI answers "is this commit good?", the deploy script answers "get this good commit onto the box safely." Conflate them and you end up unable to deploy because a runner is down.

Why not "real" CI/CD?

Because every tool you add to the release path can fail in the release path. A hosted runner puts runner availability in your critical path, your deploy secrets with a third party, and a network round-trip in front of a one-server deploy. For a fleet, blue/green, or a team where many people ship, those costs buy real things - parallelism, a marketplace of actions, audited multi-environment promotion. For one app on one server, they buy mostly overhead.

The honest trade-off:

When I'd change my mind

This is what makes it an engineering decision rather than a preference. I'd reach for a managed pipeline the moment any of these became true:

None of those are true here, so the boring script wins. The point isn't that shell scripts beat CI/CD. It's that "what does this system need?" beats "what does everyone use?" - and being able to say when you'd switch is what separates a chosen default from a cargo-culted one.

The actual lesson

The value was never the script. It's the habit of writing down what you give up, what you gain, and the conditions under which you'd decide differently. Do that for your deploy path and you'll do it for your database, your queue, and your architecture - which is most of the job.

The decision-and-tradeoffs version of this is recorded as ADR 0004, a guarded shell deploy over a hosted CI runner.

Drafted from my deploy script and working notes with help from Claude. The work and the decisions are mine; the prose is collaborative.