Another solution is to set capabilities.
On Linux, when a thread or process requires certain privilege(s) to perform an action, such as reading a file or binding to a port, it checks with a list of capabilities. If it has that capability, it'll be able to perform that function; otherwise, it can't. By default, the root user has all capabilities, for instance, the CAP_CHOWN capability, which allows it to change a file's UID and GID.
Therefore, rather than running the process as root, we can simply grant our Node process the capability of binding to privileged ports:
hobnob@hobnob:$ sudo setcap CAP_NET_BIND_SERVICE=+ep $(which node)
You can check that the capability is set for this process by running getcap:
hobnob@hobnob:$ sudo getcap $(which node)
~/.nvm/versions/node/v8.9.0/bin/node = cap_net_bind_service+ep
Now, when we run npx pm2 delete 0; yarn run serve, it'll successfully bind to port 80.
However, if we update our version of Node.js using nvm, we'd have to set the capabilities again for this new version of Node. Furthermore, this capability is not limited to binding to port 80; it's for binding to all privileged ports. This is a potential security vulnerability. Therefore, it's best not to use this approach and we should unset the capabilities:
hobnob@hobnob:$ sudo setcap -r $(which node)
hobnob@hobnob:$ sudo getcap $(which node)
[No output]