A bind mount that turned OPcache off
The php service mounted apps/. Every request stat() the tree over virtiofs. Hits in OPcache were noise.
Local TTFB on the catalog was 80 ms. The same image on the VPS, same data, sat at 40 ms until I copied the Compose file and mounted apps/ for a hotfix. TTFB jumped to 400 ms. CPU in the php container was user time on stat, not on SQL. opcache_get_status() showed misses in the thousands and hits in the tens.
opcache.validate_timestamps was 1, revalidate_freq 2. That is correct for a laptop. On virtiofs every include checks mtime through the VM. LaraBoom plus the host App is a lot of files. The worker paid that tax on every request, then ran the listing.
The problem was a bind mount that turned OPcache into a stat loop. Shop visitors waited 400 ms for HTML that used to take 40. I needed COPY in the image, validate_timestamps 0, and a rebuild for PHP changes.
Two Compose files
The public php service COPY vendor, packages/laraboom, and apps/ into the image. validate_timestamps is 0. opcache.preload loads the framework and the host App. A change in PHP is a new image. The vue service still binds frontend/ because SSR is Node and we edit Vue there.
- Dev compose keeps the bind mount and timestamps on. That is the point of local edit.
- opcache.file_cache on a volume looked clever. It still stat() the source if timestamps are on.
- nginx and php share only the unix socket volume, not the PHP tree.
TTFB on the VPS went back to the number without the mount. The hotfix process is rebuild the php image. That is slower than a bind. It is also the process that keeps OPcache doing its job.
What I took from this
opcache_get_status() hits versus misses tell the story faster than a profiler. Thousands of misses on a warm worker is timestamps plus a mount.
Dev and prod Compose must differ on the PHP tree. Copying the laptop mount to the VPS is how 40 ms becomes 400.
A PHP hotfix is an image rebuild. That is the cost of OPcache that actually caches.
