Run it as a systemd service
A server that you started by hand stops when you close the terminal. A systemd service starts it at boot, keeps it running and keeps its log. This page is for whoever manages the server, after Install on a Linux server with Node. No unit file is included in the download, so this one was written for this guide.
How this was tested. The unit below was run under systemd 252 on Debian 12 with Node 22, in a container that runs systemd. It started, ran as the choirmaster user, was restarted after its server process was killed, and stopped cleanly. Enabling it at boot was run (the link was made), but the machine was not rebooted. Adjust paths if your layout differs.
The unit file
Find where Node is installed:
command -v node
Use that path below. Put it somewhere the choirmaster user can run: a Node installed under your own home folder through a version manager is usually not.
Create /etc/systemd/system/choirmaster.service:
[Unit]
Description=Choir Master CMS
After=network-online.target
Wants=network-online.target
[Service]
User=choirmaster
Group=choirmaster
WorkingDirectory=/opt/choirmaster
EnvironmentFile=/etc/choirmaster/env
ExecStart=/usr/bin/node server/launcher.js
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
What each line does:
| Line | Why |
|---|---|
User, Group | The unprivileged user from the install page. |
WorkingDirectory | The folder the data and website paths are worked out from. Setting it makes the relative defaults safe. |
EnvironmentFile | The settings file from the install page. systemd puts each line in the environment, so PORT and DATA_DIR are real environment variables, as the launcher needs. |
ExecStart | Runs the launcher, so one-click updates work. A relative server/launcher.js is found from WorkingDirectory. |
Restart=always | See below. |
NoNewPrivileges, PrivateTmp | Basic hardening: the process cannot gain privileges, and has its own temporary folder. |
Why Restart is needed
The launcher restarts the server only when the server asks, which it does with exit code 75 after installing an update. If the server stops for any other reason, such as a crash, the launcher passes that exit on and exits too, and does nothing more. Restarting after that is the job of whatever supervises the launcher, which here is systemd. In the test, the server process was killed; the launcher exited with a failure; systemd waited five seconds and started it again.
Start it
sudo systemctl daemon-reload
sudo systemctl start choirmaster
sudo systemctl status choirmaster
The status shows the launcher and the server running under it:
● choirmaster.service - Choir Master CMS
Active: active (running) since ...
Main PID: 350 (node)
CGroup: /system.slice/choirmaster.service
├─350 /usr/bin/node server/launcher.js
└─362 /usr/bin/node /opt/choirmaster/server/entry/node.js
Then check the site answers:
curl http://localhost:3001/api/health
Start at boot
sudo systemctl enable choirmaster
Everyday commands
| Task | Command |
|---|---|
| Stop | sudo systemctl stop choirmaster |
| Start | sudo systemctl start choirmaster |
| Restart (after changing the settings file) | sudo systemctl restart choirmaster |
| Is it running? | systemctl status choirmaster |
| Stop it starting at boot | sudo systemctl disable choirmaster |
Settings are read once, when the server starts, so restart after you change /etc/choirmaster/env.
Read the log
The launcher and the server write to standard output and error, which systemd collects:
sudo journalctl -u choirmaster -f # follow it live
sudo journalctl -u choirmaster --since "1 hour ago"
sudo journalctl -u choirmaster -p warning # warnings and errors only
Lines start with a word in square brackets that says where they came from:
| Prefix | From |
|---|---|
[launcher] | The launcher: which version it starts, a restart after an update, a rollback. |
[config] | A problem with your settings, shown on every start. |
[secrets] | Settings loaded from a secrets manager or NAME_FILE files. |
[db] | The database schema being brought up to date. |
[server] | The server: it is listening, or shutting down, or failed. |
[site.config.json] | A problem in your site.config.json defaults. |
[email] | An email written to the log, only with EMAIL_PROVIDER=log. |
See also Logs and health checks.
How it stops
systemctl stop sends SIGTERM to the launcher, which passes it to the server. The server stops accepting requests, closes the database and exits, and it is forced to exit after five seconds. The log shows [server] shutting down.
Updates and the service
After a one-click update the server exits with code 75, the launcher starts the new version in the same service and checks its health for up to 90 seconds, and systemd sees nothing happen. If the new version does not report healthy, the launcher goes back to the old one. See Update your site with one click.