When the Holiday API moved to a new web server cluster with continuous deployment last weekend, the cutover happened without downtime. The new setup used symlinked release directories—a clean pattern for atomic deploys. Then edits to index.php stopped taking effect. Changes to other PHP files registered immediately. Only the document root's primary file needed a php-fpm restart to update.
The problem
The deployment flow builds a release, symlinks a "current" directory into /var/www/, and nginx serves from there. Every push went live cleanly—except when the index.php at the document root was edited. Those edits sat ignored until php-fpm was bounced. Subdirectory files updated fine. This selective failure pointed squarely at nginx configuration, not the deploy script.
Who should care
| Pattern | Impact |
|---|---|
| Self-hosted PHP on VPS with symlinked deploys | High—you hit this directly |
| Classic single-directory Apache/PHP setup | Low—no symlinks, no problem |
| Moving from copy-overwrite to atomic deploys | High—adoption risk during transition |
| Managed PHP hosting (cPanel, etc.) | Low—platform abstracts the config |
| Using Docker containers for deployment | Low—paths usually resolve inside containers |
If you recognize your setup in the top rows, audit your nginx fastcgi configuration today.
Why $document_root breaks
Nginx passes SCRIPT_FILENAME to PHP-FPM via FastCGI. The $document_root variable populates this path. Under symlinked deployments, $document_root gets resolved to an absolute path and latched in worker memory at startup—effectively cached. The fix is a one-for-one swap: replace $document_root with $realpath_root in your fastcgi_param line.
$realpath_root resolves the filesystem path on each request, bypassing the startup cache. After swapping and restarting both nginx and php-fpm, index.php updates appeared immediately. No other changes were needed.
Our analysis
Most nginx starter configs and tutorials still default to $document_root because they were written for fixed web roots. This works fine until you introduce symlinked releases. The failure is silent and selective—only files whose full path derives from $document_root hit it. Everything else resolves through $fastcgi_script_name per request.
The trade-off is real. Symlinked releases give you instant rollbacks and zero-downtime deploys. They also demand PHP-FPM opcache tuning and realpath-aware configuration. Lean single-server setups running modern deployment patterns face the worst exposure: the benefits without the process maturity to catch this class of bug quickly.
The documentation gap matters here. $realpath_root has existed in nginx for years, but it's not flagged as the preferred default in starter configs. Until that changes, every team building its own deploy pipeline will rediscover this five-line configuration bug.
Worked example
Say you move from copy-overwrite to symlinked releases on a single VPS. You create /var/www/app/releases/20240105/, point /var/www/app/current at it, and nginx serves from /var/www/app. After your first deploy, you tweak index.php and push. Nothing changes. The file on disk updated. Refreshing, hard reload, cache purge—none help.
Find fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name in your nginx server block. Change $document_root to $realpath_root. Run nginx -t, then systemctl restart nginx php-fpm. Push another index.php edit. Now it shows. That's the entire fix.
What to watch next
- Audit your nginx config for $document_root in fastcgi_param before the next mysterious index.php freeze.
- Pair the swap with short php-fpm worker lifetimes so future path resolution recycles cleanly.
- When using snippets from older guides, treat them as starting points—defaults aged, symlink support didn't improve.
Key takeaways
- $document_root latches under symlinked deployments, silently breaking index.php updates.
- $realpath_root is a drop-in replacement that resolves the path per request.
- Restart both nginx and php-fpm after changing the variable.
- The bug is selective and silent—easy to misdiagnose as a caching or deploy issue.
- Adopting atomic deploys requires auditing PHP-FPM path handling, not just the deploy script.
Frequently asked questions
Why does only index.php get stuck while other PHP files update fine?
The root file's full path derives from $document_root, which caches at worker startup under symlinks. Subdirectory files resolve through $fastcgi_script_name each request, so they refresh normally.
Do I need to restart nginx after switching to $realpath_root?
Yes. Both nginx and php-fpm need a restart so workers pick up the new FastCGI parameters. A config reload alone usually doesn't clear the cached path.
Will this affect me if I don't use symlinked deploys?
Generally no. The issue requires symlinks in the served directory path. Plain, fixed-path web roots behave normally.
Is $realpath_root safe to use on non-symlinked installs?
Yes. It resolves to the real filesystem path in all cases, so it behaves identically to $document_root in setups without symlinks.
Does this apply to opcache issues?
Different mechanism. Opcache caches compiled PHP bytecode by content; this issue is path-based. The $realpath_root fix won't resolve a separate opcache configuration problem.