The situation
A containerised Pattern Lab styleguide was crashing continuously on the test environment after a routine deployment. Docker showed the container stuck in a Restarting (Exit Code 1) loop. The logs were flooded with thousands of lines like:
restarting script because ../templates/_base/_annotations changed restarting script because ../templates/_base/_patterns changed ... Forever detected script was killed by signal: SIGKILL Script restart attempt #1 Script restart attempt #2 ...
The customer flagged it as urgent because staging and production run from the same codebase, and they were holding back a production deployment out of concern it might happen there too.
Digging into the architecture
The styleguide container runs Pattern Lab PHP, a tool that compiles Twig templates into a visual component browser — useful for developers and designers to preview UI components in isolation. Two containers share a Docker named volume:
The styleguide used forever (a Node.js process manager) with chokidar (a file-watching library) to detect changes. Every detected change sent SIGKILL to the running generator and restarted it.
The race condition
On every deployment, docker-compose down -v wipes all Docker volumes. When the stack comes back up, the shared volume is empty. The rsync container then has to write the entire templates directory into it from scratch — roughly 6,000 files.
Both containers start at the same time. chokidar initialises, scans the volume to note what's already there, then announces itself ready. From that point, any new file appearing in the volume is treated as a real change.
Those signals queue up. forever processes them one by one — kill, restart, kill, restart — with no chance for the generator to ever complete. Eventually forever gave up and exited with code 1, Docker restarted the container, and the whole cycle repeated.
The motion-sensor analogy
Think of a motion-sensor light. Every time someone walks past, the timer resets to 2 minutes. The light only turns off once nobody has walked past for a full 2 minutes — it doesn't matter if 100 people walked past, it turns off exactly once. The old file watcher had no such logic: it was more like a light that flicked off and back on for every single person who walked by.
The fix
I replaced forever's built-in file watching with a Node.js fs.watch watcher using a 500ms debounce timer. No matter how many files rsync writes, they all collapse into one regeneration triggered 500ms after the last write. A busy flag prevents a second regeneration from starting while one is already in progress. forever stayed in place, but stripped of its file-watching role — it now only handles crash recovery.
fs.watch('/srv/templates', { recursive: true }, function(event, filename) {
if (!filename) return;
if (!/\.(twig|json|md|yaml|txt)$/.test(filename)) return;
clearTimeout(timer);
timer = setTimeout(regenerate, 500); // reset on every event
});
The outcome
Key takeaways
depends_on in Docker Compose only guarantees a container has started, not that it has finished its initialisation work.Chasing a race condition between containers that "shouldn't" be talking to each other?
Get in touch