Setting Up Hot Reload for PHP and Node in Containers
AI generated
FROM
RUN
Docker · Hot Reload · PHP · Node.js
Setting Up Hot Reload for PHP
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.

15 min read Bind Mounts · OPcache · Polling · Compose Watch PHP-FPM · Vite · Node.js

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.

11. FAQ: Hot Reload for PHP and Node

1Why don't I see PHP changes immediately?
Usually disabled OPcache validation. validate_timestamps=1 and revalidate_freq=0 fix this.
2Do I always need polling?
Usually not on Linux, almost always needed on macOS and WSL2 for reliable hot reload.
3Difference between sync and sync+restart?
sync copies without restart, sync+restart copies and then restarts the affected service.
4Why is polling slower?
Active polling instead of passive events costs some CPU but works reliably across bind mounts.
5Can validate_timestamps=1 stay in production?
Technically yes, but costs performance. validate_timestamps=0 with an explicit reset on deploy is better.
6How do I check if changes reach the container?
Check with stat inside the container. If the timestamp does not change, it is the bind mount, not caching.
7Does this also work with Colima or Podman?
Yes, the principles apply regardless of the container runtime as long as bind mounts are supported.
8Does Compose Watch need a specific version?
Yes, a current Docker Compose version. Older versions use manual bind mount setup as an alternative.
9Why does Vite HMR sometimes not work?
Usually missing hmr.port/clientPort in the Vite configuration for the WebSocket connection.
10Does OPcache validation noticeably slow development?
Not noticeable with single developer requests, only relevant under load testing with many simultaneous requests.