and Node in Containers
Hot reload behaves differently in containers than on bare metal, because bind mounts, filesystem events and OPcache validation add extra layers between the editor and the running process. Understanding these layers makes PHP and Node changes appear just as fast as without containers, without rebuilding on every code change.
Table of Contents
- 1. Why hot reload is harder in containers
- 2. Configuring bind mounts correctly for PHP-FPM
- 3. OPcache validation for instant code changes
- 4. Node, Vite and polling for filesystem events
- 5. Reloading nginx and PHP-FPM without a container restart
- 6. Docker Compose Watch for automatic sync and rebuild
- 7. Hot reload on macOS and Windows in practice
- 8. Debugging hot reload issues systematically
- 9. Hot reload approaches compared
- 10. Summary
- 11. FAQ
1. Why hot reload is harder in containers
Hot reload means a code change in the editor becomes visible in the running application instantly, without manually restarting a process. On bare metal this usually works smoothly, because editor, filesystem and executing process share the same kernel and the same filesystem layer. Inside a container, extra layers are added: the bind mount between host and container, possibly a virtualized filesystem bridge on macOS or Windows, and internal caching mechanisms like OPcache for PHP, which only notice file changes after a certain interval at all.
The most common frustration with hot reload in containers happens when one of these layers is overlooked. A developer changes a PHP file, but the browser keeps showing the old state, because OPcache runs with validation enabled but the wrong interval. Or a Vite dev server inside the container does not notice file changes at all, because inotify events are not reliably passed through the bind mount, especially on macOS.
This article goes layer by layer through what is needed for reliable hot reload with PHP-FPM and Node in Docker containers, from correctly configured bind mounts through OPcache settings to polling based watchers and the newer Compose Watch feature.
2. Configuring bind mounts correctly for PHP-FPM
The basic requirement for hot reload with PHP is a bind mount that mirrors the project folder from the host directly into the container, instead of copying the code into the image at build time. If the code is baked into the image with COPY in the Dockerfile instead, the running container will never see changes to the host code, regardless of OPcache or any other setting. For development, the rule is therefore: source code is always mounted, never copied into the image.
# docker-compose.yml — development configuration with source code mounted
services:
php:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ./src:/var/www/html:cached
- ./docker/php/opcache-dev.ini:/usr/local/etc/php/conf.d/opcache-dev.ini:ro
environment:
PHP_IDE_CONFIG: "serverName=mironsoft-shop"
The cached mount option matters on macOS and tells Docker that writes from the container get priority, while reads may propagate with a slight delay. For hot reload purposes, this delay is usually unproblematic, since it is measured in milliseconds rather than seconds and barely noticeable to users.
3. OPcache validation for instant code changes
PHP-FPM caches compiled bytecode via OPcache by default, which significantly improves performance in production but is exactly the reason why hot reload seemingly does not work in development. The setting opcache.validate_timestamps controls whether PHP checks on every request if the file has changed since it was last cached. If this option is disabled, as is common in many production-like configurations, code changes stay invisible until a manual opcache_reset().
; docker/php/opcache-dev.ini — development-only OPcache settings
; Production should use validate_timestamps=0 with an explicit cache reset on deploy
opcache.enable=1
opcache.validate_timestamps=1
opcache.revalidate_freq=0
opcache.max_accelerated_files=10000
The key opcache.revalidate_freq=0 is critical for hot reload: it ensures PHP performs timestamp validation on every single request instead of only repeating it every few seconds. In production, this value would measurably degrade performance, but in local development with a single developer's requests the difference is not noticeable, yet it makes the crucial difference between instantly visible and seemingly stuck changes.
4. Node, Vite and polling for filesystem events
Node based dev servers like Vite or Webpack Dev Server normally use native filesystem events like inotify on Linux to detect changes instantly. Inside a container with a bind mount, these events work reliably on native Linux, but not always on macOS and sometimes not on Windows, because the underlying filesystem bridge does not always pass inotify events through correctly. The result: the dev server only notices file changes after a manual restart, which makes hot reload practically unusable.
{
"scripts": {
"dev": "vite --host 0.0.0.0"
}
}
// vite.config.js — enable polling as a fallback for unreliable fs events
export default {
server: {
watch: {
// Polling trades some CPU usage for reliability across bind mounts
usePolling: true,
interval: 300
},
host: true,
hmr: {
// Explicit port so HMR websocket works through the container's port mapping
port: 5173,
clientPort: 5173
}
}
}
The environment variable CHOKIDAR_USEPOLLING=true has a similar effect for tools that internally use the Chokidar library, such as older Webpack setups. Polling trades the reliability of hot reload for somewhat higher CPU usage, because the filesystem is actively polled at fixed intervals instead of passively waiting for events, which is an acceptable trade off for development environments.
5. Reloading nginx and PHP-FPM without a container restart
Changes to PHP files themselves need no restart thanks to OPcache validation, but changes to the PHP-FPM or nginx configuration do, unless precautions are taken. For hot reload friendly development, it is worth running PHP-FPM with a reload signal instead of a full restart, which does not interrupt running requests and is significantly faster than a full container restart.
# Reload PHP-FPM configuration without dropping active connections
docker compose exec php kill -USR2 1
# Reload nginx configuration the same way
docker compose exec nginx nginx -s reload
The USR2 signal to PHP-FPM gracefully restarts the worker processes without interrupting active requests, automatically picking up changed pool configurations. For the day to day hot reload workflow this is enough for the rare cases where the FPM configuration itself was actually changed, while plain PHP code changes need no restart at all.
6. Docker Compose Watch for automatic sync and rebuild
Since more recent Docker Compose versions, develop.watch provides a declarative feature that enables hot reload without additional external tools. Instead of a full bind mount, Compose Watch synchronizes changed files into the container in a targeted way, can trigger an automatic rebuild for certain file types, and distinguishes between plain file sync and a full rebuild.
# docker-compose.yml — declarative hot reload with Compose Watch
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /var/www/html
- action: rebuild
path: ./composer.json
- action: sync+restart
path: ./docker/php/php.ini
target: /usr/local/etc/php/php.ini
# Start the stack with watch mode enabled
docker compose watch
The sync action copies changed files directly into the running container without triggering a restart, ideal for PHP files with OPcache validation enabled. The rebuild action rebuilds the image completely, useful for changes to composer.json or package.json, while sync+restart syncs a file and then restarts the affected service in a targeted way. These three modes cover most hot reload scenarios without needing an external sync tool like Mutagen.
7. Hot reload on macOS and Windows in practice
On macOS and under WSL2 on Windows, the virtualized filesystem bridge between host and container adds extra latency that directly affects the perceived speed of hot reload. With current mount strategies like virtiofs on macOS, this latency drops to a level barely noticeable in daily development, whereas older mount drivers like osxfs or gRPC-FUSE used to cause noticeable delays of several seconds.
Important for hot reload specifically on these platforms: Node dev servers should almost always run with polling enabled, because native filesystem events arrive unreliably over the virtualized bridge, even with modern mount drivers. PHP changes via OPcache validation are less affected by this issue, since PHP-FPM actively checks files on every request rather than waiting for passive events.
8. Debugging hot reload issues systematically
When hot reload seemingly does not work, a systematic investigation along the involved layers helps, instead of randomly changing configuration values. The first step is always to check whether the file change reaches the container at all, independent of caching or watchers.
#!/usr/bin/env bash
# debug-hot-reload.sh — verify each layer independently
set -euo pipefail
echo "1) Does the change reach the container at all?"
docker compose exec app stat -c '%Y %n' /var/www/html/src/Controller/IndexController.php
echo "2) Is OPcache actually caching (and configured to revalidate)?"
docker compose exec app php -i | grep -E "opcache.(enable|validate_timestamps|revalidate_freq)"
echo "3) Is the Node dev server watching at all, or silently stuck?"
docker compose logs app-frontend --tail=20
echo "4) Are filesystem events reaching the container (Linux only)?"
docker compose exec app-frontend sh -c "command -v inotifywait && inotifywait -m /var/www/html/resources/js"
These four steps reliably separate whether the problem is in the bind mount, in OPcache, in the Node watcher, or in the underlying filesystem events. Most hot reload problems can be narrowed down in a few minutes with this order, instead of trying configuration files at random for hours.
9. Hot reload approaches compared
Different technologies and platforms call for different combinations of the techniques presented here.
| Scenario | Recommended approach | Latency | CPU cost |
|---|---|---|---|
| PHP on Linux | Bind mount plus OPcache validation | Very low | Negligible |
| Node on Linux | Native fs events, no polling needed | Very low | Low |
| Node on macOS/WSL2 | Enable polling | Low to medium | Increased |
| Mixed PHP/Node stack | Compose Watch with sync/rebuild/sync+restart | Low | Moderate |
For plain PHP stacks on Linux, the combination of bind mount and OPcache validation is usually sufficient and costs virtually no additional compute time. Node dev servers on macOS or under WSL2, on the other hand, almost always need polling to work reliably at all. Compose Watch is currently the most flexible solution for mixed stacks, since it can react differently depending on the file type.
Mironsoft
Docker development environments, PHP performance and Node tooling
Want to see code changes without waiting?
We set up reliable hot reload for your PHP and Node containers, from OPcache fine tuning through Compose Watch to platform specific polling configuration for macOS and WSL2.
OPcache tuning
Development friendly PHP configuration without a performance hit in production
Compose Watch setup
Setting up sync, rebuild and restart rules matched to your project
Platform optimization
Mount strategies and polling configuration for macOS and Windows/WSL2
10. Summary
Reliable hot reload in containers needs several layers to work together: a correctly configured bind mount gets code changes into the container in the first place, OPcache validation with revalidate_freq=0 ensures PHP detects these changes on every request, and polling bridges the unreliability of native filesystem events for Node dev servers on macOS and Windows. Docker Compose Watch now bundles these concepts into one declarative feature directly in the Compose file.
Anyone solving hot reload problems systematically rather than randomly first checks whether the change reaches the container at all, then the caching layer, then the watcher process. This order saves considerably more time in practice than randomly trying individual configuration values, and makes hot reload in containers just as fast as a classic local installation.
Hot Reload for PHP and Node — The Essentials at a Glance
Bind mounts
Always mount source code, never copy it into the image, or the container never sees host changes.
OPcache
validate_timestamps=1 and revalidate_freq=0 in development, never in production.
Polling
Almost always needed for Node dev servers on macOS and WSL2, since native fs events are unreliable.
Compose Watch
Declarative sync, rebuild and sync+restart rules directly in the Compose file without external tools.