Deployment and Operations
PHP has no compile step and no artifact to "launch" — the sources sit on disk and the web server reads them as they are. That is where the illusion came from that deploying PHP means git pull or rsync. It has cost the industry a great many night-time incidents, because behind the apparent simplicity sit precisely the mechanisms that make PHP fast: OPcache holds compiled code in shared memory, and the realpath cache remembers where your symlink used to point. Swap files "in place" and some requests will land in a half-copied tree — and after the swap the workers will keep executing the previous release, because that is what their cache holds.
Production PHP deployment therefore rests on three pillars, and all three are worth naming upfront. First, atomicity — a release is built into its own directory in full, and only then is a symlink switched onto it in one motion, so that no request ever sees a half-finished tree. Second, invalidation — the flip must be followed by an OPcache reset (in practice, a graceful FPM reload), or a warm cache will happily keep executing the old code indefinitely. Third, backward compatibility for the duration of the rollout — while the fleet behind the load balancer is being rolled, old and new code run against the same database at the same time, so schema changes go expand-then-contract and risky behaviour hides behind a feature flag, making a rollback a switch rather than a migration. The layer-by-layer breakdown is below.
Topic map
- Environment configuration —
.envand environment variables, why secrets are never committed, and how the CLI and FPMphp.inifiles diverge. - Deployment strategies — the atomic symlink flip, the mandatory OPcache reset, rolling a fleet behind a load balancer, and expand/contract for the schema.
- Logging and monitoring — where a warning, an uncaught exception and an application log line each go, what an error tracker adds over a log file, and what you must configure in it.
- Feature flags — where a flag's value lives, how to ramp a percentage of users stickily, and what a flag must never be.
- Scheduled tasks — how cron differs from an FPM request, how exactly one of six servers ends up running a job, and why it must be idempotent regardless.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Rolling a release out with git pull or rsync over the live directory | Requests land in a half-updated tree — a new controller with an old template |
| Flipping the symlink and calling it done | OPcache still holds the previous release's opcodes — workers keep executing the old code |
Believing ln -sfn is atomic | It unlinks and recreates the symlink; the atomic swap is mv -T (which is rename(2)) |
Forgetting the realpath cache | Even after an OPcache reset a worker may still resolve current/ to the old release directory |
| Returning an instance to the balancer as soon as the files are copied | Traffic hits a cold application with unwarmed caches — a spike of 5xx |
Shipping a DROP COLUMN in the same release as the code that stopped reading it | Instances not yet updated break on the missing column |
| "Rolling back" a migration once live data already exists under the new schema | A migration rollback is data loss; you roll back the code and leave the schema expanded |
Committing .env to the repository | Passwords and keys stay in git history forever |
Assuming there is a single php.ini | The CLI SAPI and FPM have their own; their memory_limit and max_execution_time differ |
Leaving display_errors=On in production | A stack trace with paths and a slice of config goes straight to the user's browser |
Logging by concatenation — "user $id failed" | Every message is unique, so there is nothing to group or aggregate |
Not setting release and environment in the error tracker | You have the error but no way to say which deploy introduced it |
| Using a feature flag as an authorization check | A flag is an operational switch, not a security boundary |
| Leaving a flag in the code forever | The matrix of untested combinations grows exponentially |
| Keeping cron on every machine in the fleet | The job runs on each of them — the charge goes out six times |
| Assuming a lock gives you "exactly once" | An expired lock TTL plus a retry produces a duplicate; only idempotency saves you |
| Not monitoring for a task that did not run | A silent cron looks exactly like a healthy one — you find out a month later |
Interview relevance
Deployment is the section where a senior candidate separates from a middle one in a single sentence. The question is almost always "walk me through a zero-downtime deploy", and the symlink story is only half the answer — and the easy half. The other half is the one actually being probed: the symlink is flipped, and OPcache is still holding the compiled old release, so a cache reset or a graceful FPM reload is mandatory, and until then some workers keep faithfully serving the previous version of the code. A candidate who volunteers that has almost certainly lived through the incident.
Typical checks:
- What an atomic deploy does and why the symlink flip is not the last step.
- Why an OPcache reset is required after a deploy and what happens if you skip it.
- How to roll a release across a fleet behind a balancer, and when an instance is fit for traffic again.
- How to change the schema so old and new code can run at once, and what a rollback after a migration means.
- Where a warning, an uncaught exception and an application log line go in production.
- What an error tracker gives you over
grepon a log file, and what you must configure in it. - Where a feature flag's value is stored and how a user is kept on one side of it.
- How a cron run differs from an FPM request, and how to make a job run exactly once across a fleet.
Common wrong answer: "Zero-downtime deploy is flipping the symlink, and the rest takes care of itself." The rest does not take care of itself: a warm OPcache is a cache of compiled code and it survives a file swap — moreover, the realpath cache may keep resolving current/index.php to the previous release's directory, so even an OPcache reset without recycling the workers does not always help. The second classic failure is promising to "roll the migration back" if the release goes wrong: with data already written under the new schema, rolling the migration back means losing it. You roll back the code and leave the schema expanded — that is exactly what expand/contract is for.