Linux server (systemd)

On a VM or bare metal, systemd starts the service, restarts it, collects its logs and sandboxes it. esrun's own capability checks sit inside that sandbox.

Install esrun

As root, into /usr/local/bin:

Shell
curl -fsSL https://esrun.opentechf.org/install.sh \
  | ES_RUNTIME_INSTALL=/usr/local bash -s -- --only=esrun

This installs the newest release; put ESRUN_VERSION=<version> before bash to pin one (Installation).

Then copy the bundle esdev build produced to the server, for example to /opt/app/dist.

The unit

/etc/systemd/system/app.service:

INI
[Unit]
Description=Example esrun service
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
WorkingDirectory=/opt/app
Environment=PORT=8080
ExecStart=/usr/local/bin/esrun --allow-listen=8080 --allow-env=PORT --shutdown-grace=8000 dist/server.js
Restart=on-failure
TimeoutStopSec=10
SuccessExitStatus=143
MemoryMax=512M

DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
LockPersonality=yes
SystemCallArchitectures=native
CapabilityBoundingSet=

[Install]
WantedBy=multi-user.target
Shell
systemctl daemon-reload
systemctl enable --now app

What each part is for:

SettingWhy
WorkingDirectoryesrun's file access is confined to its working directory, and it refuses to run from /
ExecStart grantsThe service's policy: only the port and variables it uses
SuccessExitStatus=143esrun exits with 143 after SIGTERM; without this a normal systemctl stop leaves the unit failed
TimeoutStopSecAbove --shutdown-grace, so requests drain before systemd sends SIGKILL
MemoryMaxThe heap ceiling is sized from it — see Memory
DynamicUser and the Protect* linesAn unprivileged user and a read-only system, whatever the code does
Do not set MemoryDenyWriteExecute

V8 compiles JavaScript to machine code at run time, which needs memory that is written and then executed. With MemoryDenyWriteExecute=yes esrun is killed with SIGTRAP as soon as it starts.

Ports below 1024

An unprivileged user cannot bind port 80 or 443. Give the service that one capability and nothing else:

INI
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

and grant the port: --allow-listen=80.

Writing files

ProtectSystem=strict makes the filesystem read-only. Give the service a directory of its own with StateDirectory=, and grant it to esrun — it lies outside the working directory:

INI
StateDirectory=app
ExecStart=/usr/local/bin/esrun --allow-listen=8080 --allow-read=/var/lib/app --allow-write=/var/lib/app dist/server.js

systemd creates /var/lib/app, owned by the service's dynamic user.

Secrets

Keep them out of the unit file, in a root-readable file:

INI
EnvironmentFile=/etc/app.env

and name each variable the service may read: --allow-env=PORT,DATABASE_URL.

Logs

esrun writes to standard output and standard error, which systemd collects:

Shell
journalctl -u app -f

Shutdown and restarts

systemctl stop app and systemctl restart app send SIGTERM. esrun stops accepting connections, finishes in-flight requests for up to --shutdown-grace milliseconds, and exits. Restart=on-failure starts it again after a crash, not after a stop.

Last updated on
Edit this page