Nginx reload failure during Let's Encrypt renewal — race condition on /run/nginx.pid in ExecReload

Environment:

  • HestiaCP on Ubuntu (systemd-based nginx service)
  • Default /usr/lib/systemd/system/nginx.service:
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStart=/usr/sbin/nginx -c ${CONFFILE}
ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /run/nginx.pid)"
ExecStop=/bin/sh -c "/bin/kill -s TERM $(/bin/cat /run/nginx.pid)"

Observed behavior:

During a normal Let’s Encrypt renewal for a single domain, v-add-letsencrypt-domain triggers multiple restart calls in quick succession:

07:51:52  v-generate-ssl-cert       'espacotres.com.br'
07:51:56  v-add-letsencrypt-domain  'espacotr' 'espacotres.com.br'
07:52:12  v-restart-service  'apache2' 'ssl'
07:52:17  v-restart-service  'nginx'   'ssl'

Inside the script, the relevant lines are:

312:  $BIN/v-restart-proxy
313:  check_result $? "Proxy restart failed" > /dev/null
345:  $BIN/v-restart-proxy
346:  check_result $? "Proxy restart failed" > /dev/null
360:  $BIN/v-restart-web
361:  check_result $? "Web restart failed" > /dev/null

So a single-domain renewal calls v-restart-proxy twice, then v-restart-web once — three separate reload/restart triggers for one certificate.

The failure:

In one of these consecutive reload cycles, the journal shows:

systemd[1]: Reloading nginx.service...
systemd[1]: Reloaded nginx.service.
systemd[1]: Reloading nginx.service...
systemd[1]: Reloaded nginx.service.
systemd[1]: Reloading nginx.service...
sh[...]: /bin/cat: /run/nginx.pid: No such file or directory
sh[...]: Usage: kill [options] <pid> [...]
systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
systemd[1]: Reload failed for nginx.service.
systemd[1]: Stopping nginx.service...
systemd[1]: nginx.service: Deactivated successfully.
systemd[1]: Starting nginx.service...

Root cause hypothesis:

ExecReload reads /run/nginx.pid via cat and pipes it into kill. When multiple restart/reload calls fire back-to-back (as v-add-letsencrypt-domain does), one of them can hit a brief window where nginx is mid-restart and the pidfile has been removed/not yet rewritten. cat then returns nothing, kill -s HUP runs with no PID argument, exits 1, and systemd marks the reload as failed — even though the underlying nginx config was valid the whole time. Systemd then stops and restarts the whole service, which succeeds, so end users see no actual outage, but the panel logs a “Nginx Restart Failed” alert.

Questions for the community / maintainers:

  1. Is the double v-restart-proxy call in v-add-letsencrypt-domain intentional, or a leftover/redundancy? Would consolidating to a single restart/reload call (or adding a short delay between the two) be an acceptable fix?
  2. Would it make sense to harden the shipped ExecReload to use nginx -s reload instead of the manual cat+kill pattern, to make it robust against overlapping calls regardless of what triggers them?
  3. As a local workaround, we applied a systemd drop-in override:
[Service]
ExecReload=
ExecReload=/usr/sbin/nginx -s reload

Does this have any known downsides in a HestiaCP context (e.g. interaction with v-restart-proxy/v-restart-web’s own status checks)?