Node not found when saving website theme

Description of the issue

in a version-15 setup, when creating a website theme, it produces the error below:
FileNotFoundError: [Errno 2] No such file or directory: 'node'

as seen in this image:

Image

then i tried to follow the stacktrace, I landed in apps/frappe/frappe/website/doctype/website_theme/website_theme.py

my assumptions are that the node executable is not loaded in the environment, so i did some quick edit to print the env to frappe throw:

Image

and i get this:

Image

since i install node using nvm, the nvm path is not present inside the env.

but then i tried to run in bench console, the env is loaded correctly:

Image

Image

even bench execute returns the same results

is something wrong somewhere?

Context information (for bug reports)

Output of bench version

5.23.0

Steps to reproduce the issue

  1. install a clean version-15 bench
  2. create a new site
  3. login to new site, go to website themes, new theme
  4. put a random name
  5. save theme

Observed result

Image

Expected result

Success

Stacktrace / full error message

Traceback (most recent call last):
  File "apps/frappe/frappe/app.py", line 114, in application
    response = frappe.api.handle(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/api/__init__.py", line 49, in handle
    data = endpoint(**arguments)
           ^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/api/v1.py", line 36, in handle_rpc_call
    return frappe.handler.handle()
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/handler.py", line 50, in handle
    data = execute_cmd(cmd)
           ^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/handler.py", line 86, in execute_cmd
    return frappe.call(method, **frappe.form_dict)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/__init__.py", line 1726, in call
    return fn(*args, **newargs)
           ^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/utils/typing_validations.py", line 31, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/desk/form/save.py", line 39, in savedocs
    doc.save()
  File "apps/frappe/frappe/model/document.py", line 342, in save
    return self._save(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/model/document.py", line 364, in _save
    return self.insert()
           ^^^^^^^^^^^^^
  File "apps/frappe/frappe/model/document.py", line 295, in insert
    self.run_before_save_methods()
  File "apps/frappe/frappe/model/document.py", line 1103, in run_before_save_methods
    self.run_method("validate")
  File "apps/frappe/frappe/model/document.py", line 974, in run_method
    out = Document.hook(fn)(self, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/model/document.py", line 1334, in composer
    return composed(self, method, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/model/document.py", line 1316, in runner
    add_to_return_value(self, fn(self, *args, **kwargs))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/model/document.py", line 971, in fn
    return method_object(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "apps/frappe/frappe/website/doctype/website_theme/website_theme.py", line 50, in validate
    self.generate_bootstrap_theme()
  File "apps/frappe/frappe/website/doctype/website_theme/website_theme.py", line 110, in generate_bootstrap_theme
    process = Popen(command, cwd=frappe.get_app_source_path("frappe"), stdout=PIPE, stderr=PIPE)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/frappe/.pyenv/versions/3.12.8/lib/python3.12/subprocess.py", line 1026, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/home/frappe/.pyenv/versions/3.12.8/lib/python3.12/subprocess.py", line 1955, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'node'

Additional information

Ubuntu 24.04 Noble Numbat, using pyenv, nvm, and bare metal install method

the interesting part is the env output the user as root, which it shouldn’t iirc

Hi,

May we know the instruction that were followed to install Frappe/Erpnext?

Does bench setup requirements complete without error?

using the installation page as a guide
using python with pyenv, node using nvm

bench setup returns no error whatsoever

this image above shows no error

supervisor also spawn processes as the user, not root

it seems like launching the wsgi server using supervisor doesn’t inherit the environment variables, especially $PATH into the process, but instead inherit the ones from root

however, when launcing wsgi (gunicorn) manually using /home/user/benches/lol-bench/env/bin/gunicorn -b 127.0.0.1:8002 -w 25 --max-requests 5000 --max-requests-jitter 500 -t 120 --graceful-timeout 30 frappe.app:application --preload, it does inherit the $PATH

What does /etc/supervisor/conf.d/frappe-bench.conf contain?

; Notes:
; priority=1 --> Lower priorities indicate programs that start first and shut down last
; killasgroup=true --> send kill signal to child processes too

; graceful timeout should always be lower than stopwaitsecs to avoid orphan gunicorn workers.
[program:lol-bench-frappe-web]
command=/home/user/benches/lol-bench/env/bin/gunicorn -b 127.0.0.1:8002 -w 25 --max-requests 5000 --max-requests-jitter 500 -t 120 --graceful-timeout 30 frappe.app:application --preload
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/web.log
stderr_logfile=/home/user/benches/lol-bench/logs/web.error.log
stopwaitsecs=40
killasgroup=true
user=user
directory=/home/user/benches/lol-bench/sites
startretries=10

[program:lol-bench-frappe-schedule]
command=/home/user/.pyenv/versions/3.12.8/bin/bench schedule
priority=3
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/schedule.log
stderr_logfile=/home/user/benches/lol-bench/logs/schedule.error.log
user=user
directory=/home/user/benches/lol-bench
startretries=10



[program:lol-bench-frappe-short-worker]
command=/home/user/.pyenv/versions/3.12.8/bin/bench worker --queue short,default
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/worker.log
stderr_logfile=/home/user/benches/lol-bench/logs/worker.error.log
user=user
stopwaitsecs=360
directory=/home/user/benches/lol-bench
killasgroup=true
numprocs=1
process_name=%(program_name)s-%(process_num)d
startretries=10

[program:lol-bench-frappe-long-worker]
command=/home/user/.pyenv/versions/3.12.8/bin/bench worker --queue long,default,short
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/worker.log
stderr_logfile=/home/user/benches/lol-bench/logs/worker.error.log
user=user
stopwaitsecs=1560
directory=/home/user/benches/lol-bench
killasgroup=true
numprocs=1
process_name=%(program_name)s-%(process_num)d
startretries=10





[program:lol-bench-redis-cache]
command=/usr/bin/redis-server /home/user/benches/lol-bench/config/redis_cache.conf
priority=1
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/redis-cache.log
stderr_logfile=/home/user/benches/lol-bench/logs/redis-cache.error.log
user=user
directory=/home/user/benches/lol-bench/sites
startretries=10

[program:lol-bench-redis-queue]
command=/usr/bin/redis-server /home/user/benches/lol-bench/config/redis_queue.conf
priority=1
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/redis-queue.log
stderr_logfile=/home/user/benches/lol-bench/logs/redis-queue.error.log
user=user
directory=/home/user/benches/lol-bench/sites
startretries=10



[program:lol-bench-node-socketio]
command=/home/user/.nvm/versions/node/v20.18.1/bin/node /home/user/benches/lol-bench/apps/frappe/socketio.js
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/user/benches/lol-bench/logs/node-socketio.log
stderr_logfile=/home/user/benches/lol-bench/logs/node-socketio.error.log
user=user
directory=/home/user/benches/lol-bench
startretries=10


[group:lol-bench-web]
programs=lol-bench-frappe-web,lol-bench-node-socketio




[group:lol-bench-workers]
programs=lol-bench-frappe-schedule,lol-bench-frappe-short-worker,lol-bench-frappe-long-worker




[group:lol-bench-redis]
programs=lol-bench-redis-cache,lol-bench-redis-queue
cat: cle: No such file or directory

i believe this is the expected behavior since supervisor master process is root, so inherently the worker processes would inherit the root user environments and run processes as the frappe user, so i might think that the way node is being called should be different in a way that it wouldn’t be invoked directly by an execute command.

If you want to see if there’s any differences , here’s one from a working install, theme seems to save ok.

erpnext 15.48.4
frappe 15.52.0
Ubuntu 24.04.1 LTS

cat /etc/supervisor/conf.d/frappe-bench.conf
; Notes:
; priority=1 → Lower priorities indicate programs that start first and shut down last
; killasgroup=true → send kill signal to child processes too

; graceful timeout should always be lower than stopwaitsecs to avoid orphan gunicorn workers.
[program:frappe-bench-frappe-web]
command=/home/frappe/frappe-bench/env/bin/gunicorn -b 127.0.0.1:8000 -w 3 --max-requests 5000 --max-requests-jitter 500 -t 120 --graceful-timeout 30 frappe.app:application --preload
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/web.log
stderr_logfile=/home/frappe/frappe-bench/logs/web.error.log
stopwaitsecs=40
killasgroup=true
user=frappe
directory=/home/frappe/frappe-bench/sites
startretries=10

[program:frappe-bench-frappe-schedule]
command=/usr/local/bin/bench schedule
priority=3
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/schedule.log
stderr_logfile=/home/frappe/frappe-bench/logs/schedule.error.log
user=frappe
directory=/home/frappe/frappe-bench
startretries=10

[program:frappe-bench-frappe-short-worker]
command=/usr/local/bin/bench worker --queue short,default
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/worker.log
stderr_logfile=/home/frappe/frappe-bench/logs/worker.error.log
user=frappe
stopwaitsecs=360
directory=/home/frappe/frappe-bench
killasgroup=true
numprocs=1
process_name=%(program_name)s-%(process_num)d
startretries=10

[program:frappe-bench-frappe-long-worker]
command=/usr/local/bin/bench worker --queue long,default,short
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/worker.log
stderr_logfile=/home/frappe/frappe-bench/logs/worker.error.log
user=frappe
stopwaitsecs=1560
directory=/home/frappe/frappe-bench
killasgroup=true
numprocs=1
process_name=%(program_name)s-%(process_num)d
startretries=10

[program:frappe-bench-redis-cache]
command=/usr/bin/redis-server /home/frappe/frappe-bench/config/redis_cache.conf
priority=1
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/redis-cache.log
stderr_logfile=/home/frappe/frappe-bench/logs/redis-cache.error.log
user=frappe
directory=/home/frappe/frappe-bench/sites
startretries=10

[program:frappe-bench-redis-queue]
command=/usr/bin/redis-server /home/frappe/frappe-bench/config/redis_queue.conf
priority=1
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/redis-queue.log
stderr_logfile=/home/frappe/frappe-bench/logs/redis-queue.error.log
user=frappe
directory=/home/frappe/frappe-bench/sites
startretries=10

[program:frappe-bench-node-socketio]
command=/usr/bin/node /home/frappe/frappe-bench/apps/frappe/socketio.js
priority=4
autostart=true
autorestart=true
stdout_logfile=/home/frappe/frappe-bench/logs/node-socketio.log
stderr_logfile=/home/frappe/frappe-bench/logs/node-socketio.error.log
user=frappe
directory=/home/frappe/frappe-bench
startretries=10

[group:frappe-bench-web]
programs=frappe-bench-frappe-web,frappe-bench-node-socketio

[group:frappe-bench-workers]
programs=frappe-bench-frappe-schedule,frappe-bench-frappe-short-worker,frappe-bench-frappe-long-worker

[group:frappe-bench-redis]
programs=frappe-bench-redis-cache,frappe-bench-redis-queue

after comparing, it is the same, only the username and paths are different, but it’s fundamentally the same

I guess it could be the node path isn’t right or a permissions issue. If these instructions were followed , maybe make sure commands not requiring root were not prefaced with sudo?

the only bench command that launched with sudo is sudo bench setup production. im sure I’ve followed all the steps required.

I was able to reproduce the error.

I entered which node and it returned /usr/bin/node and node -v returned a different version than nvm . So I had the distro node installed and Frappe was happy with that.

I apt removed nodejs and the error was there when I tried to save a theme. bench build and bench setup requirements also failed. Now it seems there is some issue with bench restart, for example, that fails with:

frappe-bench-web:frappe-bench-node-socketio: ERROR (no such file)

I’ll see if I can figure it out. No issues themes on a Docker install so far.

sorry for being late,

so it was not loaded in $PATH right. i wonder how different is a docker installation. i’ll try on figuring out how the process in docker would inherit the $PATH.

I didn’t try to fix it yet, it did work once when I put in the code from step 9 in this:

https://github.com/D-codE-Hub/Guide-to-Install-Frappe-ERPNext-in-Ubuntu-22.04-LTS

Note the source part.

In Docker which node returns /home/frappe/.nvm/versions/node/v18.18.2/bin//node

in current user shell, the $PATH is loaded correctly, however, in gunicorn’s own environment, it uses the default $PATH, which I have attached in the first post.

What I believe is that in docker, the gunicorn process itself is executed in the frappe user environment, hence why the node not found issue is not present in containerized installs. but in bare metal installations, the environment is inherited from the root user, as the supervisord master process is own by root which spawns child process as the frappe user but in the root user’s environment.

the issue is not present if I execute gunicorn manually in my own shell as the process and its environment is launched by the current shell that I’m running, which has nvm and node loaded in $PATH.

and just now, i noticed that we can set the $PATH in config/supervisor.conf to use the current shell’s $PATH by adding the lines in frappe-bench-frappe-web:

environment=PATH="/home/user/.nvm/versions/node/v20.18.1/bin:/home/user/.pyenv/plugins/pyenv-virtualenv/shims:/home/user/.pyenv/shims:/home/user/.pyenv/bin:/home/user/.pyenv/plugins/pyenv-virtualenv/shims:/home/user/.pyenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"

which makes the gunicorn process to use the designated $PATH to find the node executable in nvm

maybe we can modify bench setup supervisor to generate the supervisor config with $PATH pre-defined

The installation method from _code include the installation of npm from apt, install instructions from frappe.io do not. Installing using apt brings in node and alot of other packages, which may explain why it was working before I uninstalled it. I’ll try some other things when I have time.