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:
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:
[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
systemctl daemon-reload systemctl enable --now app
What each part is for:
| Setting | Why |
|---|---|
WorkingDirectory | esrun's file access is confined to its working directory, and it refuses to run from / |
ExecStart grants | The service's policy: only the port and variables it uses |
SuccessExitStatus=143 | esrun exits with 143 after SIGTERM; without this a normal systemctl stop leaves the unit failed |
TimeoutStopSec | Above --shutdown-grace, so requests drain before systemd sends SIGKILL |
MemoryMax | The heap ceiling is sized from it — see Memory |
DynamicUser and the Protect* lines | An unprivileged user and a read-only system, whatever the code does |
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:
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:
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:
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:
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.