If Nobody Wants to Deploy the PHP App on a Friday, the Release Path Is the Product
Release fear is a product constraint. If deploys need a checklist, a hero and a prayer, the pipeline is the work — not a new framework.
If nobody wants to deploy the PHP application on a Friday, the problem is not the calendar. The release path is the product. Features wait on it. Incidents wait on it. Upgrades wait on it. Rewrites are proposed because of it. I have sat in planning meetings where a Go migration was the suggested cure for “deploys are scary.” A new language with the same checklist, the same hand-edited production config, and the same irreversible migration is a scarier Friday.
PHP releases feel too risky to ship when the unit of deploy is “the whole monolith plus hope.” Manual checklists, no rollback, configuration that only exists on the server, migrations coupled to the release, and no way to shrink blast radius will produce superstition in any language. Fix those, and PHP 8.3 on a Thursday becomes ordinary. Skip them, and the strangler pattern becomes two systems you are afraid to ship.
This article is the release path: why the fear is rational, what the checklist is hiding, rollback, config, migrations, flags, and how to make the blast radius small enough that a grown-up team will deploy without a ritual.
Why Friday deploys became a superstition
Superstition is compressed experience. The last three production issues took hours to diagnose, needed a specific person, and could not be undone without a database conversation. The organisation learned: do not ship when that person might be at dinner. The lesson is wrong in the specific (“Fridays”) and right in the general (you cannot recover).
Typical PHP estates that learn this:
- Deploy is rsync, Capistrano, or a CI job that SSHs and
git pulls on a box. Opcache holds old files. The next request is a mix of two releases. Nobody notices until checkout. - There are two web nodes and a worker node. The deploy script updates the web nodes. The worker runs yesterday’s code against today’s schema, or the reverse.
- “The release” is a Google Doc. Step 7 is “run the SQL attached to the ticket.” Step 8 is “clear Redis if the site looks weird.” Step 9 is “ask Maria in finance to click around.”
- Staging is a database from 2023 and a
.envthat does not match production keys. Staging is green. Production is a different programme.
Fear of deploy is then used as an argument for a rewrite. The rewrite will inherit the same ops if you do not change them. A modernization audit that does not mention deploy frequency and time to recover is not an audit of a product company. It is a code review.
Hiring also hits this. Seniors will not join a team whose release process is folklore. Juniors will join and then be afraid to merge. The backlog fills with “done in staging.”
Manual checklists are not a release system
A checklist is a prototype of automation. If it is still a checklist after a year, it is the system, and it will fail when the person who wrote it is off.
Translate each line into a property of the pipeline:
- “Put the site in maintenance.” You do not have rolling deploys or compatibility between versions. Maintenance windows are a product cut. Prefer expand/contract so old and new PHP can run together for twenty minutes.
- “Run migrations.” Migrations are unattended in CI against a clone, then applied as a separate, reversible step, not a side effect of starting FPM.
- “Warm the cache / download the catalogue.” A release that requires a 12-minute warm is a release that pages people if a node rotates. Pre-warm in the new slot before you cut traffic. Or make the cache fill without a special ritual.
- “Check the homepage and checkout.” That is a smoke test. Put it in CI against staging, and again post-deploy as a synthetic. Humans are slow, expensive monitors.
- “Tell Slack it went well.” Fine. Do not make it a gate. The gate is metrics: error rate, p95, queue age, payment success.
PHP-FPM specifics people skip: opcache validate timestamps off in production means you must restart FPM or reload in a controlled way. Rolling restart without draining connections drops requests. pm.max_requests is not a deploy strategy. Shared filesystems and opcache are a classic split-brain.
Workers: Messenger, Horizon, queue:work, cron. If they are not deployed and restarted in the same story as FPM, you will run mixed versions. Mixed versions plus a queue is duplicate processing or poison payloads. Include workers in the release unit or split their contract so mixed versions are safe.
The test that matters is: a new engineer, following the README, can ship a no-op change to production in working hours and roll it back without calling you. If that sentence is funny, you have found the work.
Rollback, config, and migrations as the real surface
Rollback. If rollback is “deploy the previous git tag” and that tag cannot run on the current schema, you do not have rollback. You have a forward-only tape. Split schema from code. Expand first (additive migration, old code ignores new columns). Deploy code. Contract later (remove columns when no release still reads them). Keep the previous container image. Point the load balancer back. That is rollback. Anything that requires restoring a database dump is a disaster recovery drill, not a release abort.
Config. If production has credentials and feature state that only exist on the box, every deploy is a snowflake. Config belongs in the environment or a vault, injected at start, identical across nodes. The .env on the server that someone edited in 2021 during an incident is now load-bearing. Find it. That file is why staging lies.
PHP apps accumulate config.php that if (gethostname() === 'prod-2'). That is not configuration. That is a map of outages. Replace it with explicit env vars and fail to boot if a required one is missing. Silent defaults in production are how you send mail through the staging SMTP on a bad day.
Secrets. They should rotate without a code change. They should not live in the repo. They should not live in a screenshot in the wiki. This is not a lecture about security theatre. It is why a rollback cannot recreate production.
Migrations. Doctrine and Laravel will both happily generate a destructive diff. Review SQL. Run it in a transaction where the engine allows. For MySQL, know which alters lock the table and how big the table is. A “simple” column add on a 200 million row table is the release. The PHP files are not. Schedule it. Practice on a restored backup. If you have never timed it, you do not have an estimate.
Store migrations in one place. Two apps writing migrations for the same database — the PHP monolith and a new Go service — need a single sequential history. Otherwise you have two sources of truth and a Friday.
Feature flags and shrinking the blast radius
The blast radius of a PHP monolith deploy is “all customers, all features.” Flags cut that without microservices.
Use flags for behaviour, not for hiding a broken compile. The app must boot with the flag off. The dangerous path is gated. Roll out 1%, 10%, 50% if you have a way to bucket users stably. If you do not, at least gate by staff, then by a customer list, then everyone.
Flags need an owner and an expiry. A flag that has been “temporary” for eighteen months is another config snowflake. Removing the old path is part of the release system.
Where flags are the wrong tool: schema incompatibility, payment provider cutovers that cannot dual-run, and anything that is not actually toggleable because both paths share a global cache key. In those cases, shrink by workload: isolate the worker, the admin, the read-only report. The strangler approach is blast-radius management. It is wasted if you cannot ship the monolith weekly before you add a second binary.
Canary a single node. Send 5% of traffic. Watch error rate and the metrics you would use if you suspected slowness. If you cannot send 5% — one load balancer, sticky sessions that pin everyone to node A — that is also release work. Stickiness plus a canary is how you canary nothing.
Blue/green with PHP is ordinary now: two slots, migrate expand, start the new slot, healthcheck, switch, keep the old slot warm for an hour. The reason teams do not do it is usually the stateful bits: uploaded files on local disk, cron on one box, a cache that is not shared. Those are the real blockers. Name them.
Make the money path observable, or you will keep needing Maria
If the only way to know a release is good is a human clicking checkout, you will never ship at 16:00. You will wait for Maria.
Minimum:
- Synthetic checkout (or your equivalent) from outside the cluster, on a schedule, with alerting.
- Payment success rate as a metric, not a feeling.
- Queue age and DLQ depth.
- Error rate by route, not a single global 500 counter that hides the refund endpoint.
- Logs that include a request id the support team can ask for.
PHP has had APM for years. Use it. If traces are sampled too low to see a 1% canary, raise them for the canary window.
On-call needs a rollback button that is not “find the person with production SSH.” The button can be a pipeline “redeploy previous image.” Practice it on a staging slot until it is boring.
Deploy frequency is a leading indicator. Teams that ship daily learn that releases are small. Teams that ship quarterly learn that releases are projects. Projects fail in interesting ways. If you currently ship monthly, do not aim for twelve times a day. Aim for weekly, then twice a week, with the same checklist shrinking each time.
Fix the release path before you modernise the stack
Unsupported PHP, abandoned packages, and a rewrite proposal all sit on top of this. If you cannot ship, you cannot upgrade. If you cannot upgrade, someone will sell you a greenfield. Greenfield with the same ops is a longer superstition.
Sequence:
- One artefact, one pipeline, FPM and workers in the same story.
- Config and secrets out of the snowflake server.
- Additive migrations separated from code.
- Smoke and synthetics on the money path.
- Previous image retained; abort is a switch.
- Then PHP version, then framework, then any extract.
A PHP modernization audit that puts “deploy and rollback” in the first pages is the useful kind. One that skips to language choice is not.
If you want the release path described as a sequence of weeks against your actual nodes, checklists and money path, the Migration Readiness Audit is €1,200 and is allowed to conclude that the next quarter is pipeline work, not Go. PHP modernisation implementation is €402/day. PHP→Go at €402/day stays later, for a slice with a flag you would actually press on a Friday. I would rather you ship PHP on a Thursday than collect another architecture slide.