Skip to content

Running Node Daemons on Adobe Commerce Cloud

Several Hyvä features are powered by a long-running Node.js daemon that the PHP application contacts over a local socket at render time. They all follow the same pattern: a Node HTTP server bound to 127.0.0.1 in the web container, called synchronously by php-fpm while a page is being rendered.

Known Hyvä daemons:

Daemon Module Contacted by PHP at... Default bind Purpose
CMS Tailwind compiler hyva-themes/magento2-cms-tailwind-compiler CMS content preview and save in the admin 127.0.0.1:3200 Compile CMS Tailwind CSS server-side
Response minifier hyva-themes/magento2-minification every frontend HTML response 127.0.0.1:3100 Minify HTML + inline CSS/JS (@swc/html)

More may follow. Everything on this page applies to any of them: Node installation, the process-reaping limitation, and the launch requirement are identical. Where they differ (port, config namespace, authentication, fallback), see the per-daemon reference at the end.

Why Adobe Commerce Cloud needs special handling

On a normal server these daemons can be started on-demand by PHP or by Cron, and they keep running once started. On Adobe Commerce Cloud that does not work: the platform actively terminates processes started from transient scopes. Working around that is what this page is about.

TL;DR

  • Node.js is not in the Cloud PHP container by default. Add it with dependencies.nodejs in .magento.app.yaml, so it is on PATH at build and runtime.
  • Any Hyvä daemon must run in the web container. php-fpm contacts it on 127.0.0.1, so it has to share that container's loopback (and its own module code and node_modules).
  • The built-in daemon_management modes do not work on Cloud without a support ticket. A daemon started from cron:run (cron) or a web request (on_demand) is SIGKILLed when that transient scope is torn down. setsid does not help.
  • A daemon started detached from an interactive SSH session persists. This proves the platform reaps transient cron/request scopes, not long-running processes in the container's main scope.
  • So keeping a daemon alive on Cloud takes an Adobe support request. Either approach works: ask Adobe to enable long-running processes started from the Magento cron (what they did for another customer), or to provision a supervised process in the web container's main scope. Both require support.

Requirements for All Daemons

Every Hyvä Node daemon on Adobe Commerce Cloud has the same two requirements:

  • Node.js 20 or newer.
  • It runs in the web container. php-fpm contacts the daemon on 127.0.0.1:<port> or a socket, so the daemon must live in the same container as php-fpm. It also needs its own module files present on that filesystem, and it writes runtime state under var/. The CMS Tailwind compiler additionally reads the deployed theme directories (vendor/hyva-themes/**/web/tailwind, generated/ sources and .phtml templates). None of them is a decoupled service, so being HTTP-reachable from elsewhere is not sufficient.

Installing Node.js on Adobe Commerce Cloud

Node is not part of the Cloud PHP container. Declare it as a dependency in .magento.app.yaml:

dependencies:
    php:
        composer/composer: '2.x'
    nodejs:
        npm: "*"

Declared dependencies are on PATH both at build time and at runtime, unlike a Node installed ad-hoc in the build hook (for example via nvm), which exists only during the build. Verify it after deploy:

magento-cloud ssh -e <env> -- 'node -v; command -v node'

Bundled vs. Installed Node Dependencies

Some daemons ship their production Node dependencies pre-bundled (the CMS Tailwind compiler); others require an npm install (the response minifier). Because the runtime filesystem is read-only, run that install in the build hook so node_modules is baked into the image:

hooks:
    build: |
        set -e
        composer install --no-dev
        # ... storefront Tailwind build ...
        # For daemons that need their own node_modules (e.g. the minifier):
        npm --prefix vendor/hyva-themes/magento2-minification/node install --ignore-scripts
        php ./vendor/bin/ece-tools run scenario/build/generate.xml
        php ./vendor/bin/ece-tools run scenario/build/transfer.xml

Never commit a locally-built node_modules

Daemon dependencies such as @swc/html ship platform-specific native binaries. A node_modules installed on macOS, Windows, or arm64 will fail to load on Cloud's linux-x64 container (wrong or missing binary). Always run npm install --ignore-scripts in the Cloud build hook so the correct binaries are fetched for the runtime platform, and keep node_modules git-ignored (the module's node/.gitignore already excludes it).

Separate from the storefront Tailwind build

This is separate from the build-time Tailwind compile of the storefront theme, which also needs Node in the build hook. See Adobe Commerce Cloud Deployment.

Enabling and Configuring a Hyvä Daemon

Each daemon has an on/off (or strategy) switch, a daemon-management mode, and a transport block in env.php. The config paths differ per daemon, so check the per-daemon reference. In general:

  • Daemon management uses <namespace>/general/daemon_management, one of off, cron, or on_demand.
  • Transport is an <namespace> block in app/etc/env.php:
'<namespace>' => [
    'transport'   => 'tcp',            // or 'unix_socket'
    'tcp_host'    => '127.0.0.1',
    'tcp_port'    => <port>,
    // 'socket_path' => '/tmp/hyva-<daemon>.sock',
    'node_binary' => 'node',           // resolved from PATH by default
]

Authentication is daemon-specific. The CMS Tailwind daemon uses a bearer token (persisted in var/hyva_cms_tailwind_daemon.json); the minifier has no auth and relies solely on binding to 127.0.0.1. Do not expose any of them beyond loopback.

The Core Problem: Cloud Reaps Cron and Request-Scoped Processes

None of the built-in daemon_management modes keeps a Hyvä daemon alive on Adobe Commerce Cloud:

  • cron - the watchdog cron (run by bin/magento cron:run) starts the daemon when it is down. The daemon binds its port and logs a "listening" line, then is killed within about 60 seconds. A fresh instance is spawned every cron cycle, giving a new PID each minute.
  • on_demand - a web request spawns the daemon on first use. Same fate.

The Daemon Is Not Self-Terminating

The reaping described below is a platform behavior that applies to any long-running process, so it holds for every Hyvä daemon regardless of its internals:

  • No idle or inactivity timeout.
  • No parent-liveness (ppid) check.
  • The shutdown handler logs Shutting down... for SIGTERM, SIGINT, and a poison-pill restart flag.

The daemon log shows only the "listening" line, never Shutting down.... So the process is not catching a signal and exiting gracefully; it is being SIGKILLed externally (uncatchable, therefore unlogged).

setsid Does Not Escape the Reaping

The daemon is spawned with setsid ... < /dev/null (new session/PGID, detached stdin), which is the correct way to detach a background process. It still does not stop the reaping, because the kill happens at the cgroup / scope level: when a cron:run (or request) finishes, the platform SIGKILLs that run's control group. setsid changes the session, not cgroup membership, so it cannot escape. Launching with the older nohup ... & does not help either, since that only makes the process ignore SIGHUP.

The Reaping Is Scope-Specific, Not Universal

Here is the decisive test. Start a daemon detached from an interactive SSH session, then disconnect. This example uses the CMS Tailwind daemon; the minifier is analogous:

magento-cloud ssh -e <env>
SCRIPT=vendor/hyva-themes/magento2-cms-tailwind-compiler/node/daemon.mjs
export TAILWIND_COMPILER_AUTH_TOKEN=$(php -r 'echo json_decode(@file_get_contents("var/hyva_cms_tailwind_daemon.json"),true)["auth_token"] ?? bin2hex(random_bytes(32));')
setsid node "$SCRIPT" < /dev/null >> var/manual-daemon.log 2>&1 &
pgrep -af daemon.mjs      # note the pid
exit                       # disconnect - removes the launching session

Reconnect a few minutes later and the daemon is still running. Detached from an interactive session it is reparented to PID 1 and lives in the container's persistent main cgroup, which the platform does not tear down.

Conclusion: the platform reaps transient cron/request scopes, not long-running processes in the container's main scope. The Hyvä daemons are fully viable on Cloud; they just have to run in a context the platform does not reap. The built-in cron/on-demand launchers do not provide that by default, so it takes an Adobe support request (see below).

Ways to Keep a Daemon Running

Interim or Demo: Manual Start

The setsid start shown above persists until the next deploy or container restart. Set that daemon's daemon_management to off first, so the watchdog does not spawn colliding instances (each collision logs EADDRINUSE because the port is already held).

This is fine for a demo, but it is lost on every deploy and it is not supervised.

Production: Ask Adobe to Keep the Process Alive

Keeping a daemon alive in production means getting the platform to stop reaping it, which requires an Adobe support request. There are two known approaches, and either one works:

  • Enable long-running processes started from the Magento cron. Adobe can configure the environment so a process started by bin/magento cron:run is not torn down with the cron scope. The built-in cron daemon-management mode then keeps the daemon alive on its own, with no other changes. Adobe has done this for another customer.
  • Provision a supervised process in the web container's main scope. Adobe starts and supervises the daemon directly in the web container, outside any cron or request scope.

In both cases the daemon must run in the web container. A Cloud worker is not suitable: workers run in a separate container, so the worker's 127.0.0.1:<port> is not the web container's loopback, and the daemon must be co-located with php-fpm (and, for the CMS Tailwind daemon, the theme filesystem).

Fallback: Run Without the Daemon

Each daemon has a non-daemon fallback for when a supervised process cannot be run:

  • CMS Tailwind: set compilation_strategy to in-browser (Tailwind v4 compiles in the browser).
  • Minifier: set hyva_minification/general/enabled to 0 (responses are served un-minified). Inline JavaScript merging, also offered by the module, runs entirely in PHP and can still be used without the daemon.

Adobe Support Request Template

Use this template when opening the support request to provision a persistent daemon.

Support request template

On Adobe Commerce Cloud (2.4.x), I need to run a Hyvä Node daemon: a persistent Node HTTP server on 127.0.0.1:<port> in the web container that the Magento PHP application contacts over loopback, co-located with the application (it needs its own module files on the same filesystem and writes runtime state under var/). Node is already available at runtime via dependencies.nodejs.

Verified on this environment: a daemon started detached (setsid, stdin redirected) from an interactive SSH session persists indefinitely, but the same daemon started from cron:run or a web request is SIGKILLed when that scope is torn down (no Shutting down... log; setsid does not help, as it is a cgroup-level kill). So the platform reaps transient cron/request scopes but not the container's main scope.

Please make such a daemon survive on this environment. Either would work: enable long-running processes started from the Magento cron (bin/magento cron:run), so the process it launches is not reaped, or provision a supervised, persistent Node process in the web container (main scope) directly. In both cases it must be reachable by php-fpm on 127.0.0.1:<port>. This has been set up for other customers. What configuration do you need from me?

Diagnostic Reference

Replace <port> with the daemon's port (3200 CMS Tailwind, 3100 minifier).

# Is a daemon process running / listening?
pgrep -af 'daemon.mjs|minifier'
ss -ltnp | grep <port>

# The reaping fingerprint: a NEW "listening" line roughly every minute, and no
# "Shutting down..." line, means it is being SIGKILLed and respawned by cron.
tail -f var/log/hyva_*_daemon.log

# Health/connectivity (CMS Tailwind exposes an unauthenticated GET /health;
# other daemons may differ - a plain TCP connect still proves reachability)
curl -m 30 -sS -i http://127.0.0.1:<port>/health

# Confirm how the deployed launch detaches the daemon (setsid by default)
grep -rn 'setsid\|nohup' vendor/hyva-themes/*/src/Model/DaemonManager.php

Per-Daemon Reference

This table summarizes where the CMS Tailwind compiler and the response minifier differ.

CMS Tailwind compiler Response minifier
Module hyva-themes/magento2-cms-tailwind-compiler hyva-themes/magento2-minification
Contacted at CMS content preview and save in the admin Every frontend text/html response
Default bind 127.0.0.1:3200 127.0.0.1:3100
env.php namespace hyva_cms_tailwind hyva_minification
Enable / strategy hyva_cms_tailwind/hyva_cms_bridge/compilation_strategy = daemon | in-browser hyva_minification/general/enabled = 1 | 0
Daemon management hyva_cms_tailwind/general/daemon_management hyva_minification/general/daemon_management
Authentication Bearer token in var/hyva_cms_tailwind_daemon.json None (localhost binding only)
Node deps Pre-bundled (no npm install) npm install --ignore-scripts required
Manual start node vendor/hyva-themes/magento2-cms-tailwind-compiler/node/daemon.mjs (with TAILWIND_COMPILER_AUTH_TOKEN) npm --prefix vendor/hyva-themes/magento2-minification/node run minifier-daemon
Env vars TAILWIND_COMPILER_AUTH_TOKEN MINIFIER_PORT, MINIFIER_HOST, MINIFIER_SOCKET, MALLOC_ARENA_MAX
Non-daemon fallback compilation_strategy to in-browser enabled to 0