@push.rocks/smartdaemon 🚀
Turn your Node.js scripts into production-ready system daemons
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Seamlessly manage long-running processes, background services, and system daemons with smart root detection, automatic privilege escalation, and systemd integration.
🎯 What is SmartDaemon?
@push.rocks/smartdaemon is your Swiss Army knife for turning Node.js applications into bulletproof system services. Whether you're deploying microservices, running scheduled tasks, or managing background workers, SmartDaemon handles the complex stuff so you can focus on your business logic.
✨ Key Features
- 🔐 Smart Privilege Management: Automatically detects root status and handles sudo operations seamlessly
- 👤 User/Group Control: Run services under specific users for enhanced security
- 🔄 Systemd Integration: Full Linux systemd support for production-grade service management
- 🛡️ Auto-Recovery: Built-in service restart on failure
- 📝 Declarative Service Definitions: Simple, clear service configuration
- 🔌 Interactive & Non-Interactive Modes: Support for both passwordless sudo and interactive authentication
- 🎛️ Complete Lifecycle Management: Start, stop, enable, disable, and reload services with ease
📦 Installation
# Using npm
npm install @push.rocks/smartdaemon --save
# Using pnpm (recommended)
pnpm add @push.rocks/smartdaemon
# Using yarn
yarn add @push.rocks/smartdaemon
🚀 Quick Start
Basic Usage
import { SmartDaemon } from '@push.rocks/smartdaemon';
// Initialize SmartDaemon
const daemon = new SmartDaemon();
// Create and enable a service
const myService = await daemon.addService({
name: 'my-api-server',
description: 'My awesome API server',
command: 'node server.js',
workingDir: '/opt/myapp',
version: '1.0.0'
});
// Enable and start the service
await myService.enable(); // Creates systemd service file
await myService.start(); // Starts the service
Running Services as Specific Users 👥
Security best practice: Never run services as root unless absolutely necessary!
const webService = await daemon.addService({
name: 'web-server',
description: 'Production web server',
command: 'node app.js',
workingDir: '/var/www/app',
version: '2.1.0',
user: 'www-data', // Run as www-data user
group: 'www-data' // Run under www-data group
});
await webService.enable();
await webService.start();
🔐 Smart Privilege Escalation
SmartDaemon intelligently handles privilege escalation based on your execution context:
Automatic Root Detection
// SmartDaemon automatically detects if running as root
const daemon = new SmartDaemon();
// If running as regular user, sudo will be used automatically
// If running as root, commands execute directly
With Sudo Password
If you're not running as root and don't have passwordless sudo configured:
const daemon = new SmartDaemon({
sudoPassword: 'your-sudo-password' // Will be used for privilege escalation
});
// All privileged operations will now use the provided password
const service = await daemon.addService({
name: 'privileged-service',
description: 'Service requiring root privileges',
command: 'node admin-task.js',
workingDir: '/opt/admin',
version: '1.0.0'
});
await service.enable(); // Automatically uses sudo with password
Passwordless Sudo (Recommended for Production)
For production environments, configure passwordless sudo for specific systemctl commands:
- Create a sudoers file:
/etc/sudoers.d/smartdaemon - Add the following content:
# Allow smartdaemon to manage services without password
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl start smartdaemon_*
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop smartdaemon_*
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable smartdaemon_*
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable smartdaemon_*
yourusername ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
📚 Complete API
SmartDaemon Class
interface ISmartDaemonOptions {
sudoPassword?: string; // Optional sudo password for privilege escalation
}
class SmartDaemon {
constructor(options?: ISmartDaemonOptions);
// Add a new service or update existing one
async addService(options: ISmartDaemonServiceOptions): Promise<SmartDaemonService>;
// Direct access to managers (advanced usage)
systemdManager: SmartDaemonSystemdManager;
templateManager: SmartDaemonTemplateManager;
}
Inspecting and controlling an existing systemd unit
SystemdUnit works with an exact canonical .service name. Construction performs
no I/O. It does not create or rewrite a unit, enable it, remove a mask, or invoke
sudo. The caller must already have permission for lifecycle commands.
import { SystemdUnit } from '@push.rocks/smartdaemon';
const unit = new SystemdUnit({ unitName: 'smartdaemon_my-api-server.service' });
const state = await unit.inspect();
console.log(state.loadState, state.activeState, state.unitFileState);
// Address the calling user's manager when running as that user.
const userUnit = new SystemdUnit({ unitName: 'authswitch.service', scope: 'user' });
const userState = await userUnit.inspect();
// Application code decides whether stopping or restarting is appropriate.
// await unit.stop();
// await unit.start();
Snapshots are immutable and also include subState, mainPid, controlPid,
controlGroup, killMode, job, statusText, result and fileDescriptorStoreCount. An empty job means no
systemd job was reported; otherwise it is the pending job's id. statusText is the
service's last STATUS= notification, verbatim (empty when it sent none), and result
is systemd's Result: success, or why the service last failed (timeout,
exit-code, signal, core-dump, watchdog, start-limit-hit, oom-kill,
resources, protocol, exec-condition; a newer manager's word is still reported).
fileDescriptorStoreCount is how many descriptors the manager holds in the service's file
descriptor store right now (NFileDescriptorStore): 0 for a service without a store and for a
missing or masked unit, and possibly above 0 for a stopped service whose store is preserved.
Missing and masked units remain distinct from inactive or disabled units. Aliases
resolving to a different canonical unit are rejected.
start() and stop() await the systemctl command and return a fresh snapshot.
They do not establish application readiness or prove database shutdown; callers
must apply the owning application's lifecycle contract. A command failure or
timeout rejects with SystemdUnitError and a local code, without returning raw
command diagnostics. The systemd job can continue after a client timeout, so a
failed command must not be treated as cancelled or successfully completed.
A Type=notify start returns only at READY=1, which can outlast commandTimeoutMs.
start({ wait: false }) and stop({ wait: false }) pass --no-block: systemd
verifies and enqueues the job, and the call returns the snapshot read right after,
typically activating or deactivating with the job pending. A refused job (a
missing or masked unit, a conflicting transaction) still rejects with
command_failed. The caller then polls inspect(): job empties when the job
finishes, statusText carries the service's progress, and result tells a start
timeout (timeout) from a crash (exit-code, signal, core-dump). Options other
than a boolean wait reject with invalid_options before any command runs.
const pending = await unit.start({ wait: false });
// Later, on the caller's own schedule:
const state = await unit.inspect();
if (state.job === '' && state.activeState === 'failed') {
console.log(state.result === 'timeout' ? 'start stalled' : `crashed: ${state.result}`);
} else {
console.log(state.activeState, state.statusText);
}
Calls use an absolute systemctlPath (default /usr/bin/systemctl), a clean
environment, no shell or password prompt, and bounded output. Trusted owning code
can choose another absolute executable path and commandTimeoutMs (default
120000, maximum 600000). This timeout bounds the client process only.
The default scope: 'system' retains system-manager behavior. scope: 'user'
uses systemctl --user as the calling user. Every SystemdUnit operation contacts
the user manager, so each requires a real, owned, private XDG_RUNTIME_DIR (or an
explicit runtimeDirectory) and passes that directory with the user's home and any
set XDG_CONFIG_HOME and XDG_DATA_HOME to the command. Before each call, all of
them must be absolute, normalized paths without a trailing slash, and the runtime
directory is checked again on disk; a missing, malformed, replaced, linked or
group-accessible runtime directory, a malformed XDG root and a missing or malformed
home directory each fail with invalid_options before contacting systemd.
Construction validates only the options it is given: the unit name, systemctlPath,
busctlPath, commandTimeoutMs, scope and a supplied runtimeDirectory, which must be a
well-formed path (and is refused for system scope). It reads, but does not validate,
the process environment above, so a unit can be constructed without a login session
(cron, a plain ssh command) or with a malformed XDG variable; its calls then refuse.
An empty XDG variable counts as unset. The environment is captured at construction;
later changes to process.env do not affect an existing unit.
cleanFileDescriptorStore() releases the service's file descriptor store with
systemctl clean --what=fdstore, closing every descriptor the manager holds for it, and returns a
fresh snapshot; nothing else of the unit is cleaned. The manager refuses, and the call rejects with
command_failed, unless the unit is loaded, inactive (a failed unit needs systemctl reset-failed first) and without a pending job, and its loaded FileDescriptorStoreMax is above
zero or the store still holds a descriptor. Once the manager has accepted the clean, every outcome
states that the store was released: the call resolves with the snapshot read afterwards, or, when
that inspection fails, rejects with released_uninspected and the inspection's error as cause.
The snapshot reports the unit as it is when read: its fileDescriptorStoreCount is 0 unless the
unit was started right after the release and stored descriptors again, which a caller must not
read as a failed clean. A refused clean, or one that could not be run, rejects with
command_failed; so does a clean whose systemctl call exceeded commandTimeoutMs, whose outcome
is unknown because the manager may have released the store before the client was killed. After
command_failed a caller must inspect() the unit before treating the store as kept. Cleaning a
store needs systemd 254 or later: the call reads the manager's version first and refuses an older
manager with SystemdUnitManagerTooOldError (manager_too_old), an unreadable version with
version_unknown and an unanswered read with command_failed, each without running the clean.
inspectConfiguration() reports the selected fragmentPath, opaque dropInPaths
list and needsReload flag. reloadConfiguration() explicitly reloads the manager's
unit definitions globally and returns a fresh configuration snapshot. Neither
operation changes enablement or masks.
inspectServiceSettings() reports the loaded service settings a definition renders, drop-ins
included: type, notifyAccess, restart, killMode (systemd's own words; type is empty
for a unit systemd could not load), delegate, restartPreventExitStatus and
successExitStatus (each { statuses, signals }: exit statuses ascending, and signals by
systemd's name without SIG, such as TERM, which only a hand-written unit or drop-in sets),
startLimitBurst, startLimitIntervalMicroseconds (0 when the start rate limit is disabled),
restartMicroseconds, timeoutStopMicroseconds and timeoutStartMicroseconds (exact integers or 'infinity';
the start timeout is the loaded TimeoutStartSec=, without any EXTEND_TIMEOUT_USEC=
extension a running start requested), limitNoFile and limitNoFileSoft
(hard and soft), tasksMax (integers or 'infinity'), oomScoreAdjust, fileDescriptorStoreMax
(0 without a store), fileDescriptorStorePreserve (no, yes or restart) and environment, the
unit's own Environment= variables as a frozen name-to-value object (variables the manager gives
every unit, EnvironmentFile= and PassEnvironment= are not included). A missing, repeated,
foreign or malformed property rejects with invalid_state; this includes an Environment= value
holding a control character, which systemctl show prints as a C escape. inspectServiceSettings()
needs systemd 254 or later, the first to report FileDescriptorStorePreserve: it reads the manager's
version first and refuses an older manager with SystemdUnitManagerTooOldError (manager_too_old)
and an unreadable version with version_unknown, both before the unit is read (see The manager's
version below). Every environment a SystemdServiceDefinition accepts
reads back. Reload changed files first; the snapshot describes what the manager loaded.
execStart lists every ExecStart= command of the loaded unit, drop-ins included, in the order
the manager runs them, each as a frozen ISystemdExecCommand: path (the executable), argv
(the argument vector, argv[0] included, one string per argument exactly as loaded; %
specifiers resolved as systemd loaded the unit, $VARIABLES and $$ as written) and ignoreFailure (a - prefix). It is empty for a unit
that sets none or that the manager could not find. systemctl show joins argv with spaces, so
an argument holding a space could not be told apart there; this field is read instead from the
unit's D-Bus ExecStart property (a(sasbttttuii)) with busctl --json=short get-property,
through an absolute busctlPath (default /usr/bin/busctl, systemd 240 or newer) with the same
clean environment and timeout. The system scope reads the system bus; the user scope reads the
user's bus at $XDG_RUNTIME_DIR/bus, so a user manager without a user D-Bus fails
inspectServiceSettings() with command_failed. A failed bus read rejects with
command_failed, and a reply that is not exactly that signature with an absolute path, a
non-empty argv of strings and a boolean flag per command rejects with invalid_state.
const settings = await new SystemdUnit({ unitName: 'pallet-containerd.service' }).inspectServiceSettings();
const carriesContract = settings.type === 'notify' && settings.notifyAccess === 'all' &&
settings.killMode === 'process' && settings.delegate && settings.limitNoFile === 'infinity' &&
settings.limitNoFileSoft === 'infinity' && settings.tasksMax === 'infinity' &&
settings.oomScoreAdjust === -999 && settings.restart === 'always' &&
settings.restartMicroseconds === 2_000_000 && settings.timeoutStopMicroseconds === 60_000_000;
// The exact command line the manager runs, argument by argument.
const runsServe = settings.execStart.length === 1 &&
settings.execStart[0].argv.join('\0') === ['/opt/pallet/pallet-control', 'containerd-serve'].join('\0');
enable() persistently enables the existing unit definition and returns a fresh
state snapshot. It does not rewrite the definition, start the unit, unmask it or
force conflicting links. Run it inside the owning installer's cross-process
installation scope, after any required offline commissioning. A failed or timed
out command can leave partial enablement; inspect and retry the exact operation
without assuming cancellation or undoing the application's retained protection.
inspectRelationships() returns immutable sorted arrays for before, after,
requires, wants, conflicts, requiredBy and wantedBy from the loaded manager
graph. Literal systemd hex escapes are preserved. The bounded snapshot rejects
missing fields, foreign identities, duplicate relationships and malformed names.
unitFileState === 'enabled' proves only that some enablement links exist. For a
boot guard, verify that its requiredBy and before both include every required
target and the actual network provider; inspect the provider's requires and
after as well. Recheck the exact file digest, selected fragment, absence of
drop-ins and needsReload === false before permitting dependent activation.
The manager's version
inspectManagerVersion() reads the running manager's version through the unit's scope — the
system manager, or the calling user's manager for scope: 'user' — with systemctl show --property=Version, the same systemctlPath, environment and timeout as every other call. It
resolves with a frozen ISystemdManagerVersion: major (255) and version, the manager's
Version property verbatim (255.4-1ubuntu8.17). This is the running manager's version, which after
a package upgrade differs from systemctl --version until the manager re-executes. A manager that
does not answer rejects with command_failed; an answer that is not a single Version= line with a
version starting with a major of one to four digits rejects with version_unknown.
requireManagerVersion(requiredMajor) resolves with that version when its major is at least
requiredMajor (an integer from 1 to 9999; anything else rejects with invalid_options before any
command) and otherwise rejects with a SystemdUnitManagerTooOldError: a SystemdUnitError with code
manager_too_old that carries the managerVersion it read and the requiredMajor. Calls that need a
newer manager use it and fail closed: inspectServiceSettings(), cleanFileDescriptorStore() and
installing a definition with a requiredManagerMajor (see SystemdUnitFile.install()), so none fails
late with a generic code. runInTransientScope() refuses an older user manager the same way with its
own SystemdTransientScopeManagerTooOldError.
import { SystemdUnit, SystemdUnitManagerTooOldError } from '@push.rocks/smartdaemon';
const unit = new SystemdUnit({ unitName: 'pallet-node.service' });
console.log((await unit.inspectManagerVersion()).major); // 255
try {
await unit.inspectServiceSettings();
} catch (error) {
if (error instanceof SystemdUnitManagerTooOldError) {
// e.g. 'systemd 253 (253.5-1) running, 254 required'
console.error(`systemd ${error.managerVersion.major} (${error.managerVersion.version}) running, ` +
`${error.requiredMajor} required`);
}
throw error;
}
Installing a direct systemd service definition
SystemdServiceDefinition encodes a literal executable and argument list, with
explicit restart policy, kill mode and stop timeout. System-scope definitions require
user and group; user-scope definitions reject both directives because the user
manager already runs under the calling identity. It defaults to Type=exec,
journal output and null stdin. It introduces no shell; percent specifiers and
argument environment expansion are escaped. Paths must be absolute and normalized.
Descriptions, paths and arguments reject control characters, lone surrogates and Unicode
noncharacters (U+FDD0–U+FDEF and every code point ending in FFFE or FFFF), which systemd
does not accept as UTF-8 clean and would ignore, leaving the unit unloadable. Descriptions and working directories cannot have
backslashes or surrounding whitespace; executable paths also reject quotes and
backslashes. Arguments may contain spaces, quotes, backslashes and empty strings.
import { SystemdServiceDefinition, SystemdUnitFile } from '@push.rocks/smartdaemon';
const definition = new SystemdServiceDefinition({
unitName: 'smartdaemon_my-api-server.service',
description: 'My API server',
executable: '/opt/my-api/bin/server',
args: ['serve'],
workingDirectory: '/opt/my-api',
user: 'root',
group: 'root',
restart: 'no',
killMode: 'mixed',
timeoutStopSeconds: 360,
});
const file = new SystemdUnitFile({ unitName: definition.unitName });
// Inside the privileged installer's existing cross-process ownership scope:
const previous = await file.inspect();
const installed = await file.install(definition, previous?.sha256 ?? null);
console.log(installed.changed, installed.masked, installed.record.sha256);
For a user service, give the definition and installer the same scope:
const userDefinition = new SystemdServiceDefinition({
scope: 'user',
unitName: 'authswitch.service',
description: 'Account authority',
executable: '/opt/authswitch/bin/authswitch',
args: ['daemon'],
workingDirectory: '/opt/authswitch',
restart: 'on-failure',
killMode: 'mixed',
timeoutStopSeconds: 60,
});
const userFile = new SystemdUnitFile({ scope: 'user', unitName: userDefinition.unitName });
const previousUserFile = await userFile.inspect();
await userFile.install(userDefinition, previousUserFile?.sha256 ?? null);
User definitions default to WantedBy=default.target and have no system-only
After=local-fs.target edge. User unit files default to
$XDG_DATA_HOME/systemd/user (or ~/.local/share/systemd/user), below the user's
config directory in the unit load path. Installation and enablement use the
current user manager and do not escalate privileges. The installer rejects unsafe
ownership or group-writable path components; prepare those directories explicitly
before installation. It never changes the permissions of existing directories.
A user-scope SystemdUnitFile validates each value where it is read:
- Construction validates the unit's options as
SystemdUnitdoes, a suppliedunitDirectoryandexpectedUid(which must equal the caller's uid). WithoutunitDirectory, it also derives the publicpathfromXDG_DATA_HOME, or from the home directory when that is unset, and rejects a missing or malformed source there withinvalid_options; no other process value is validated at construction. inspect()reads only the unit directory as that uid. It needs no user manager, no runtime directory and no XDG config root, so with an explicitunitDirectoryit works whateverXDG_CONFIG_HOME,XDG_DATA_HOME,XDG_RUNTIME_DIRand the home directory hold.install()admits, reloads and verifies through the manager and recognizes the user's persistent and runtime mask paths. It therefore needs everything the manager calls need, plus a well-formed config root (XDG_CONFIG_HOME, or~/.config), and rejects anything missing or malformed withinvalid_optionsbefore touching any directory or runningsystemctl.
An owner can therefore ask whether its definition is installed from a context without a login session:
// Resolves to null when the file is absent, or { path, sha256 } for a protected definition.
const installed = await new SystemdUnitFile({ scope: 'user', unitName: 'authswitch.service' }).inspect();
inspect() and install() check every component of the unit directory from /
down, then the unit file itself. A refusal is a SystemdUnitFileUnsafePathError, a
SystemdUnitFileError with code unsafe_path that names the offending path and a
reason of type TSystemdUnitFileUnsafePathReason, so the owner knows exactly what
to fix:
reason |
path |
Refused because |
|---|---|---|
unsupported_platform |
unit directory | the process does not run on Linux |
foreign_process |
unit directory | the process does not run as the expected uid |
symlink |
directory component or unit file | it is a symbolic link |
not_directory |
directory component | it is not a directory |
foreign_owner |
directory component or unit file | a directory is owned by neither root nor the expected uid, or the file not by the expected uid |
group_or_world_writable |
directory component | it is group- or world-writable (only a root-owned mode-1777 /tmp is admitted) |
unreadable |
unit file | it cannot be opened; the operating-system error is the cause |
not_regular_file |
unit file | it is not a regular file |
hard_linked |
unit file | it has more than one hard link |
wrong_mode |
unit file | its mode is not exactly 0644 |
too_large |
unit file | it is larger than 16384 bytes |
replaced |
unit directory or a file being written | it changed identity between two checks during install() |
import { SystemdUnitFileUnsafePathError } from '@push.rocks/smartdaemon';
try {
await file.inspect();
} catch (error) {
if (error instanceof SystemdUnitFileUnsafePathError) {
// e.g. 'group_or_world_writable' '/home/user/.local' after `chmod 0775 ~/.local`
console.error(error.reason, error.path);
}
throw error;
}
For a boot prerequisite that must finish before dependent services start, supply
startup: { type: 'oneshot', remainAfterExit: true, timeoutStartSeconds: 120 }.
Systemd waits for successful process exit; RemainAfterExit keeps the completed
unit active. The start timeout is an integer from 1 to 600 seconds. Oneshot
services reject restart: 'always'; the default exec definition remains unchanged.
A long-running service that reports its own readiness uses
startup: { type: 'notify', notifyAccess: 'all', timeoutStartSeconds: 120 }, rendered as
Type=notify, NotifyAccess= and TimeoutStartSec=. Systemd counts the start complete when a
process admitted by notifyAccess sends READY=1: 'main' admits only the main process,
'exec' also the processes of the unit's Exec*= commands, 'all' every process in the unit's
cgroup, as a ready notification from a child of the main process needs. NotifyAccess=none is
not offered because it discards every notification. Notify services may use restart: 'always'.
These optional settings render after TimeoutStopSec= in this order, and nothing when omitted,
so existing definitions keep their exact bytes:
| Option | Directive | Accepted values |
|---|---|---|
restartSeconds |
RestartSec= (after Restart=) |
integer 1–600; refused with restart: 'no' |
restartPreventExitStatus |
RestartPreventExitStatus= (after RestartSec=) |
distinct integers 0–255, at least one, rendered ascending; refused with restart: 'no' |
successExitStatus |
SuccessExitStatus= (after RestartPreventExitStatus=) |
distinct integers 0–255, at least one, rendered ascending |
startLimitIntervalSeconds |
StartLimitIntervalSec= in [Unit], after the relations |
integer 0–86400; 0 disables the start rate limit |
startLimitBurst |
StartLimitBurst= in [Unit], after StartLimitIntervalSec= |
integer 1–1000; refused with startLimitIntervalSeconds: 0 |
delegate |
Delegate= |
boolean |
limitNoFile |
LimitNOFILE= (soft and hard) |
positive integer or 'infinity' |
tasksMax |
TasksMax= |
positive integer or 'infinity' |
oomScoreAdjust |
OOMScoreAdjust= |
integer −1000–1000 |
environment |
one Environment= line per variable, ordered by name |
plain object of string values; see below |
const containerd = new SystemdServiceDefinition({
unitName: 'pallet-containerd.service',
description: 'Pallet containerd',
executable: '/opt/pallet/current/pallet-control',
args: ['containerd-serve'],
workingDirectory: '/',
user: 'root',
group: 'root',
restart: 'always',
restartSeconds: 2,
killMode: 'process',
timeoutStopSeconds: 60,
startup: { type: 'notify', notifyAccess: 'all', timeoutStartSeconds: 120 },
dependencies: {
defaultDependencies: true,
after: ['pallet-guard.service'],
before: ['pallet-node.service'],
requires: ['pallet-guard.service'],
wants: [],
conflicts: [],
},
delegate: true,
limitNoFile: 'infinity',
tasksMax: 'infinity',
oomScoreAdjust: -999,
});
A service that restarts on failure but must not loop on a permanent refusal names the exit
statuses that end it for good, and disables the start rate limit so the manager's
DefaultStartLimitIntervalSec=/DefaultStartLimitBurst= cannot make it give up during a long
outage of something it depends on:
const node = new SystemdServiceDefinition({
unitName: 'spark.service',
description: 'serve.zone Spark node',
executable: '/opt/spark/current/spark',
args: ['runnode'],
workingDirectory: '/opt/spark',
user: 'root',
group: 'root',
restart: 'on-failure',
restartSeconds: 5,
killMode: 'mixed',
timeoutStopSeconds: 360,
restartPreventExitStatus: [78], // EX_CONFIG: a permanent refusal
startLimitIntervalSeconds: 0,
});
renders StartLimitIntervalSec=0s as the last [Unit] line and
Restart=on-failure, RestartSec=5s, RestartPreventExitStatus=78 in [Service]. Systemd 230
and later read the start rate limit from [Unit] only; under systemd 255 systemd-analyze verify
loads such a unit without a warning.
Systemd applies these within what the manager itself may grant. Under systemd 255 in an
unprivileged container, this unit started with the manager's own open-file hard limit for
LimitNOFILE=infinity and without the lowered OOM score, and still reported active.
inspectServiceSettings() reports the configured values, not the running process's limits.
environment gives the service's processes literal variables, for example the installer's PATH
when the executable or the tools it starts live outside the manager's default search path (a user
manager's default PATH omits ~/.local/bin and nvm's Node directories):
const authority = new SystemdServiceDefinition({
scope: 'user',
unitName: 'authority.service',
description: 'Authority daemon',
executable: '/home/owner/.nvm/versions/node/v24.9.0/bin/node',
args: ['/home/owner/app/cli.js', 'serve'],
workingDirectory: '/home/owner',
restart: 'on-failure',
killMode: 'mixed',
timeoutStopSeconds: 30,
environment: { PATH: process.env.PATH!, CODEX_HOME: '/home/owner/.codex' },
});
// Environment="CODEX_HOME=/home/owner/.codex"
// Environment="PATH=/home/owner/.nvm/versions/node/v24.9.0/bin:/home/owner/.local/bin:/usr/bin:/bin"
The value must be a plain object (not a proxy, class instance or null-prototype object) whose own
properties are enumerable data properties. Names match [A-Za-z_][A-Za-z0-9_]* and have at most
128 characters; values are strings of at most 4096 characters without control characters, lone surrogates or
Unicode noncharacters, and may be empty. At most 64 variables and 8192 UTF-8 bytes of NAME=value in total are
accepted, and the whole unit stays within 16384 bytes. Anything else throws
SystemdServiceDefinitionError. Each assignment is double-quoted with \ and " backslash-escaped
and % doubled, so systemd applies no specifier; Environment= never expands $, so values are
taken literally. An empty object renders nothing. Systemd merges these variables over its own
(HOME, USER, INVOCATION_ID, …), so a name such as NOTIFY_SOCKET overrides the manager's value.
Notify access and the file descriptor store
A service that keeps a descriptor across its own restarts, such as a network namespace a child
process created, hands it to the manager with sd_notify FDSTORE=1 (optionally FDNAME=), and
the manager passes it back to the next main process through LISTEN_FDS/LISTEN_FDNAMES:
| Option | Directive | Accepted values |
|---|---|---|
notifyAccess |
NotifyAccess=, right after the startup lines |
'none', 'main', 'exec' or 'all'; refused beside a notify startup, which names its own |
fileDescriptorStoreMax |
FileDescriptorStoreMax= (after OOMScoreAdjust=) |
integer 1–65536 |
fileDescriptorStorePreserve |
FileDescriptorStorePreserve= (after FileDescriptorStoreMax=) |
'no', 'yes' or 'restart'; requires fileDescriptorStoreMax |
'main' admits notifications from the main process only and 'exec' from the main and control
processes the manager started; a descriptor a child of the main process stores needs 'all'.
Without notifyAccess systemd 255 admits the main process for a service with a store, and it
loads an explicit NotifyAccess=none beside a store as main, so 'none' is refused there.
fileDescriptorStorePreserve: 'no' releases the store when the service stops, 'restart'
(systemd's default) once it is neither active nor about to restart, and 'yes' only when the
unit leaves the manager's memory or SystemdUnit.cleanFileDescriptorStore() releases it. A reboot
empties the store; a systemctl soft-reboot keeps the running kernel, so a preserved store and the
descriptors in it pass on to the next userspace boot. Systemd reads FileDescriptorStorePreserve= from version 254
and an older manager would ignore the line, so a definition with fileDescriptorStorePreserve has
requiredManagerMajor 254 and SystemdUnitFile.install() refuses it on an older manager with
SystemdUnitManagerTooOldError (manager_too_old) before touching anything; every other definition
has requiredManagerMajor null. Sending FDSTORE=1 needs an AF_UNIX socket, so a definition
with a store or a notify access other than 'none' that restricts its socket families must allow
AF_UNIX. Omitted, each option renders nothing.
const node = new SystemdServiceDefinition({
unitName: 'pallet-node.service',
description: 'serve.zone Pallet node',
executable: '/opt/pallet/current/pallet-control',
args: ['runtime-unit'],
workingDirectory: '/',
user: 'root',
group: 'root',
restart: 'on-failure',
restartSeconds: 5,
killMode: 'mixed',
timeoutStopSeconds: 360,
notifyAccess: 'all',
fileDescriptorStoreMax: 1,
fileDescriptorStorePreserve: 'yes',
});
// Type=exec, NotifyAccess=all, …, FileDescriptorStoreMax=1, FileDescriptorStorePreserve=yes
Under systemd 255 systemd-analyze verify loads such a unit without a warning.
Credentials, a dynamic user and the sandbox
A system service can receive secrets as systemd credentials instead of variables, run as a
transient account and be confined. Each option below renders nothing when omitted, so existing
definitions keep their exact bytes; false renders the directive as no. A user service refuses
all of them, since a user manager cannot grant them. DynamicUser= follows Group=; the others
follow the environment lines in this order:
| Option | Directive | Accepted values |
|---|---|---|
dynamicUser |
DynamicUser= |
boolean; with true, user and group name the allocated account |
loadCredentials |
one LoadCredential= line per credential, ordered by name |
array of { name, source? } |
loadCredentialsEncrypted |
one LoadCredentialEncrypted= line per credential, ordered by name |
array of { name, source? } |
noNewPrivileges |
NoNewPrivileges= |
boolean |
protectSystem |
ProtectSystem= |
boolean, 'full' or 'strict' |
protectHome |
ProtectHome= |
boolean, 'read-only' or 'tmpfs' |
privateTmp, privateDevices, protectKernelTunables, protectKernelModules, protectKernelLogs, protectControlGroups, protectClock, protectHostname, restrictNamespaces, restrictRealtime, restrictSUIDSGID, lockPersonality |
the directive of the same name, in this order | boolean |
capabilityBoundingSet |
CapabilityBoundingSet= |
distinct capabilities from systemdCapabilities, sorted; empty drops every capability |
ambientCapabilities |
AmbientCapabilities= |
distinct capabilities, sorted; within capabilityBoundingSet when both are given |
restrictAddressFamilies |
RestrictAddressFamilies= |
at least one distinct family of AF_INET, AF_INET6, AF_NETLINK, AF_PACKET, AF_UNIX, AF_VSOCK, sorted; includes AF_UNIX for a notify service |
const relay = new SystemdServiceDefinition({
unitName: 'relay.service',
description: 'Cluster relay',
executable: '/opt/relay/current/relay',
args: [],
workingDirectory: '/opt/relay/current',
user: 'relay',
group: 'relay',
dynamicUser: true,
restart: 'always',
restartSeconds: 2,
killMode: 'mixed',
timeoutStopSeconds: 60,
dependencies: {
defaultDependencies: true,
after: ['network-online.target'],
before: ['node.service'],
requires: [],
wants: ['network-online.target'],
conflicts: [],
},
environment: { RELAY_BIND_PORT: '8443' },
loadCredentials: [{ name: 'RELAY_AUTHORIZATION' }],
noNewPrivileges: true,
protectSystem: 'strict',
protectHome: true,
privateTmp: true,
privateDevices: true,
capabilityBoundingSet: [],
restrictAddressFamilies: ['AF_INET', 'AF_INET6', 'AF_UNIX'],
});
// DynamicUser=yes
// LoadCredential=RELAY_AUTHORIZATION
// CapabilityBoundingSet=
// RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
A credential's value never appears in the unit, in the service's environment (so child processes
do not inherit it) or in systemctl show: the service reads it from
$CREDENTIALS_DIRECTORY/<name>, readable by the service's user only. name is 1–255 characters of
[A-Za-z0-9_.@-], not starting with .. source is either an absolute normalized path of
[A-Za-z0-9_.@/+-] characters without a trailing / (a file, a directory of files or an AF_UNIX
socket), or a credential name that systemd looks up among its own credentials and then in
/etc/credstore/, /run/credstore/ and /usr/lib/credstore/ (for encrypted credentials also the
credstore.encrypted directories). Without source, systemd looks up name itself, so
{ name: 'RELAY_AUTHORIZATION' } reads /etc/credstore/RELAY_AUTHORIZATION; a source equal to
name is refused, as it only spells the same lookup twice. These characters need no quoting and
hold no % specifier, : separator or escape, so systemd reads each line exactly as written.
Encrypted credentials are sealed with systemd-creds encrypt (host key or TPM2). Systemd keeps both
kinds in one table by name, so a name appears in at most one list; at most 64 per list. The
directory search needs systemd 250 or later; an absolute source works from systemd 247.
dynamicUser: true allocates the account when the service starts. Systemd would use a static
account of the same name instead, so user and group must not be root, and they have at most
31 characters, the longest name systemd allocates. For such a service systemd enforces
ProtectSystem=strict, PrivateTmp=yes, NoNewPrivileges=yes, RestrictSUIDSGID=yes and at least
ProtectHome=read-only; a definition that states any of them weaker is refused, so the unit never
reads as less confined than it is.
Each refusal throws SystemdServiceDefinitionError, whose option names the offending option (for
every option of a definition, not only these) and whose message is
Invalid direct systemd service definition: <option>.. option is 'options' for the definition
object itself: not a plain object, an unknown or accessor key, or a unit over 16384 bytes. No
refusal carries the offending value.
Optional dependencies describes the complete literal runtime relationships:
dependencies: {
defaultDependencies: false,
after: [],
before: ['network-pre.target', 'shutdown.target', 'systemd-networkd.service'],
requires: [],
wants: ['network-pre.target'],
conflicts: ['shutdown.target'],
},
install: { wantedBy: [], requiredBy: ['network-pre.target', 'systemd-networkd.service'] },
System definitions always retain After=local-fs.target. Disable default dependencies only for
an intentionally early boot or late shutdown service. Every list is bounded to
32 distinct literal unit names, sorted deterministically; templates, specifiers,
self references and contradictory direct relationships are rejected. These local
checks do not prove the complete host dependency graph is free of cycles.
Consumers must declare both requires: ['the-bootstrap.service'] and
after: ['the-bootstrap.service'] for a failed bootstrap to prevent their start.
Ordering alone does not propagate failure. A prerequisite must actually fail its
start on failure; a skipped condition is not equivalent. Reverse stop ordering
keeps the prerequisite active until its ordered consumers stop.
The example assumes the host uses systemd-networkd.service: requiring the guard
only from network-pre.target does not block a network manager that merely orders
itself after that target. Select and verify the actual host network owner's edges.
install supplies only WantedBy/RequiredBy metadata, with at least one target.
The privileged installer must separately enable and verify the intended links;
rendering or installing the file does not activate them. A successful oneshot
proves only that its executable returned success, so that executable must finish
and verify all required work before exiting. See the upstream
service readiness
and dependency semantics.
test/native/qualify-systemd.py exercises the generated units under Ubuntu systemd
PID 1 across provisioning, successful cold boot and failed cold boot. It checks
actual networkd failure coupling and reverse shutdown order in a disposable QEMU
overlay without an external network backend. The fixture requires QEMU/KVM,
OVMF, xorriso, Python PyYAML and global tsx. Supply the unmodified
Ubuntu Noble 20260826 image
with SHA-256 d0fe84bb5f80853425fa6be28e2c106f30104c3cfe8611933f2e65c9b63f0e30:
python3 test/native/qualify-systemd.py --image /path/to/noble-server-cloudimg-amd64.img \
--work-root /tmp/smartdaemon-systemd-qualification-local
To exercise the public enablement and relationship APIs in the same guest, bundle
test/native/activate-systemd.ts as a self-contained Node ESM file and also supply
--node /path/to/node --activation-bundle /path/to/activate.mjs. Any library absent
from the minimal image can be supplied explicitly with repeatable
--node-library /path/to/libname.so.N. These hashed test inputs travel on a
read-only ISO. The API fixture checks enablement without implicit start, oneshot
readiness, unchanged definition/configuration, detection of a missing provider
link despite enabled state, exact enable retry and preservation of a mask.
Installation requires Linux, the relevant manager's user identity and a quiescent
exact unit with no process,
cgroup or job. It rejects effective drop-ins and shadowing fragments. Definitions
default to /usr/local/lib/systemd/system for system scope and the user's data
unit directory for user scope, below administrator or user configuration in the
load path, so existing persistent/runtime masks remain intact. Existing
enablement is preserved; an absent unit becomes disabled. It never enables,
unmasks, starts or stops a service, or invokes sudo.
Masked system units may report /dev/null or their exact administrator mask path:
/etc/systemd/system/<unit> for a persistent mask, /run/systemd/system/<unit>
for a runtime mask. Installation preserves that exact manager-selected fragment
through the write and reload, and never replaces the masking fragment itself.
For user scope, the exact user config or runtime mask path and the global user-unit
mask paths are also recognized.
Pending manager reloads are rejected before writing; inspect and explicitly
reload an existing changed configuration before retrying installation.
Each of these manager checks refuses with a SystemdUnitFileUnsafeUnitError, a
SystemdUnitFileError with code unsafe_unit whose reason of type
TSystemdUnitFileUnsafeUnitReason says what to resolve. It also carries the
state snapshot (ISystemdUnitState) the check read and, for the configuration
reasons, the configuration snapshot (ISystemdUnitConfigurationState), otherwise
null:
reason |
configuration |
Refused because |
|---|---|---|
active |
null |
the unit has a main or control process or a control group, or is neither inactive/dead nor failed/failed: stop it first |
job_pending |
null |
a systemd job is queued for the otherwise quiet unit |
unexpected_load_state |
null |
the load state is none of loaded, masked and not-found |
unexpected_file_state |
null |
the unit-file state does not fit the load state (loaded: enabled, enabled-runtime or disabled; masked: masked or masked-runtime; not-found: none) |
reload_pending |
snapshot | the manager needs a daemon-reload for the unit |
drop_ins |
snapshot | the unit has effective drop-in files (dropInPaths) |
unexpected_fragment |
snapshot | the selected fragmentPath is not the unit path (none for an absent unit), or a masked unit's is neither /dev/null nor its matching mask path, or is the unit path itself |
import { SystemdUnitFileUnsafeUnitError } from '@push.rocks/smartdaemon';
try {
await file.install(definition, installed?.sha256 ?? null);
} catch (error) {
if (error instanceof SystemdUnitFileUnsafeUnitError && error.reason === 'active') {
// e.g. 'active' 'running' 4711: the service must be stopped before its definition is replaced
console.error(error.state.activeState, error.state.subState, error.state.mainPid);
}
throw error;
}
The same checks rerun after the manager reload; a refusal there is reported as
reload_failed, because the new definition is already in place, and the
SystemdUnitFileUnsafeUnitError is its cause. Every other reload_failed also
keeps the underlying error as cause, such as the SystemdUnitError of a failed
systemctl daemon-reload.
The caller must already serialize privileged installation across processes;
SystemdUnitFile only excludes simultaneous operations on the same instance.
The expected previous SHA-256 is mandatory (null requires an absent file),
while already matching bytes are idempotent. Installation checks protected
directories and regular single-link mode-0644 files, retains changed definitions
as <unit>.previous-<sha256>, flushes them before atomic replacement, reloads
the manager and checks its selected fragment and final bytes. A reload failure
leaves the new file and retained previous definition in place; recovery requires
inspection, never an automatic rollback. A definition whose requiredManagerMajor is not null is
admitted only on a manager at least that new, read with SystemdUnit.requireManagerVersion() before
any directory, file or other systemctl effect; that read's refusal propagates unchanged as the
unit's SystemdUnitError: manager_too_old (a SystemdUnitManagerTooOldError), version_unknown,
or command_failed when the manager does not answer. Filesystem and definition errors use
SystemdUnitFileError with a local code; every unsafe_path refusal is a
SystemdUnitFileUnsafePathError naming the offending path and its reason, and
every unsafe_unit refusal is a SystemdUnitFileUnsafeUnitError naming its reason
with the manager snapshots it read. Trusted qualification code can set
unitDirectory, expectedUid and the inherited SystemdUnit command options;
user scope requires expectedUid to match the caller's actual uid. The manager's
selected fragment must match the installed file after reload, so an unrecognized
custom user unit path fails closed.
With killMode: 'mixed', the main process owns graceful child shutdown and must
remain alive until its children have joined. Systemd sends SIGTERM to the main
process first and can send SIGKILL to the whole cgroup when the main process exits
or the stop deadline expires. control-group sends the initial signal to the
whole group. process signals only the main process, on stop and on a stop timeout; every
other process in the unit survives, so the main process must own their shutdown or their
deliberate retention (as with Delegate=yes workloads). A successful systemd start still requires application readiness
verification, and a completed stop alone does not prove database drain.
Starting a program in its own transient scope
A service that launches a long-lived program keeps that program in its own
control group, so stopping the service ends the program too, even when the
program detached itself with setsid. runInTransientScope() starts a program in
a new transient scope unit of the calling user's systemd manager instead
(systemd-run --user --scope): the program and every process it starts run in
that scope and outlive the caller's unit. The call waits for the program's direct
process and returns its result; a program that daemonizes and exits returns while
its daemon keeps the scope active, and the scope ends when its last process exits.
import { runInTransientScope, SystemdTransientScopeError } from '@push.rocks/smartdaemon';
try {
const result = await runInTransientScope({
scope: 'user',
description: 'Codex app-server daemon',
executable: '/home/owner/.local/bin/codex',
args: ['app-server', 'daemon', 'start'],
workingDirectory: '/home/owner',
environment: { HOME: '/home/owner', CODEX_HOME: '/home/owner/.codex', PATH: '/usr/bin:/bin' },
});
console.log(result.unitName, result.exitCode, result.stdout);
} catch (error) {
if (error instanceof SystemdTransientScopeError && error.code === 'user_manager_unavailable') {
// No user manager: the caller decides how to run the program without one.
}
throw error;
}
The result holds the scope's unitName, the direct process's exitCode (or the
signal that ended it), and its stdout and stderr, UTF-8 decoded. A failure
status is a result, not an error. The program is executed directly from an
absolute executable with literal args, with no shell, PATH search or $
expansion (systemd 258 expands $ in a scope's command line unless told not to,
and the call always passes --expand-environment=no), in the absolute
workingDirectory, with stdin on /dev/null. Its
environment is exactly environment plus the manager's XDG_RUNTIME_DIR and
systemd's INVOCATION_ID: nothing of the calling process is inherited, so pass
every variable the program needs. XDG_RUNTIME_DIR, INVOCATION_ID and names
starting with SYSTEMD_ or DBUS_, which would configure systemd-run itself, are
refused. unitName names the scope (a .scope name) and defaults to a unique
smartdaemon-<32 hex digits>.scope; a scope of that name that is still active
refuses a second start. The literal description is the unit's description.
Failures throw SystemdTransientScopeError with the scope's unitName (null for
refused options), the stderr read so far and one of these codes:
| Code | Meaning |
|---|---|
invalid_options |
Malformed options, or a runtime directory that is not a private directory of the caller |
unsupported_platform |
The process is not running on Linux |
user_manager_unavailable |
No XDG_RUNTIME_DIR (or runtimeDirectory), a runtime directory that does not exist, or a manager that does not answer systemctl --user show --property=Version with a version as SystemdUnit.inspectManagerVersion() reads it |
manager_too_old |
The user manager is older than systemd 254; a SystemdTransientScopeManagerTooOldError naming its managerVersion and the requiredMajor. Nothing was started |
scope_start_failed |
systemd-run exited without reporting the scope running; the program has not run |
timed_out |
The manager probe or the program did not finish and close its output within commandTimeoutMs |
output_limit |
The program's stdout or stderr exceeded 1 MiB |
The call completes once the direct process has exited and its stdout and stderr
have closed, so a daemon must not keep them open (Codex, for example, redirects
its daemon's output); otherwise the call runs into its timeout. On timed_out and
output_limit the direct process is killed and the processes it started stay in
the scope. commandTimeoutMs (default 120000, maximum 600000) bounds the probe
and the program each. Trusted owning code can set runtimeDirectory and the
absolute systemdRunPath and systemctlPath. systemd-run reports the started
scope as its first stderr line, which systemd 254 and later print, and systemd-run
before 254 lacks --expand-environment. The manager probe's version stands for the
installation's: a user manager older than 254 is refused with manager_too_old
before systemd-run starts. A systemd-run older than the manager it addresses still
fails with scope_start_failed.
If the program cannot be executed after the scope started, systemd-run reports
that as the program's exit status 1.
SmartDaemonService Class
interface ISmartDaemonServiceOptions {
name: string; // Unique service identifier
description: string; // Human-readable description
command: string; // Command to execute
workingDir: string; // Working directory (absolute path)
version: string; // Service version
user?: string; // User to run service as (optional)
group?: string; // Group to run service as (optional)
}
class SmartDaemonService {
// Lifecycle management
async enable(): Promise<void>; // Install and enable service
async disable(): Promise<void>; // Disable service
async start(): Promise<void>; // Start service
async stop(): Promise<void>; // Stop service
// Service management
async save(): Promise<void>; // Save service configuration
async delete(): Promise<void>; // Remove service
async reload(): Promise<void>; // Reload systemd daemon
}
🎭 Real-World Examples
Microservice Deployment
import { SmartDaemon } from '@push.rocks/smartdaemon';
async function deployMicroservices() {
const daemon = new SmartDaemon();
// API Gateway
const apiGateway = await daemon.addService({
name: 'api-gateway',
description: 'API Gateway Service',
command: 'node --max-old-space-size=2048 gateway.js',
workingDir: '/opt/services/gateway',
version: '3.2.1',
user: 'apiuser',
group: 'apigroup'
});
// Auth Service
const authService = await daemon.addService({
name: 'auth-service',
description: 'Authentication Service',
command: 'node auth-service.js',
workingDir: '/opt/services/auth',
version: '2.1.0',
user: 'authuser',
group: 'authgroup'
});
// Database Sync Worker
const dbWorker = await daemon.addService({
name: 'db-sync-worker',
description: 'Database Synchronization Worker',
command: 'node workers/db-sync.js',
workingDir: '/opt/services/workers',
version: '1.5.3',
user: 'dbworker',
group: 'dbgroup'
});
// Enable and start all services
const services = [apiGateway, authService, dbWorker];
for (const service of services) {
await service.enable();
await service.start();
console.log(`✅ Service ${service.name} is running`);
}
}
deployMicroservices();
Scheduled Task Runner
async function setupScheduledTasks() {
const daemon = new SmartDaemon();
// Backup service that runs every night
const backupService = await daemon.addService({
name: 'nightly-backup',
description: 'Nightly database backup service',
command: 'node --require dotenv/config backup-runner.js',
workingDir: '/opt/backup',
version: '1.0.0',
user: 'backup',
group: 'backup'
});
await backupService.enable();
await backupService.start();
}
Development vs Production Setup
import { SmartDaemon } from '@push.rocks/smartdaemon';
async function setupService(environment: 'development' | 'production') {
let daemonOptions = {};
if (environment === 'development') {
// In development, you might use sudo password
daemonOptions = {
sudoPassword: process.env.SUDO_PASSWORD
};
}
// In production, rely on passwordless sudo configuration
const daemon = new SmartDaemon(daemonOptions);
const service = await daemon.addService({
name: `app-${environment}`,
description: `Application ${environment} server`,
command: environment === 'production'
? 'node --production app.js'
: 'node --inspect app.js',
workingDir: '/opt/application',
version: '1.0.0',
user: environment === 'production' ? 'appuser' : process.env.USER,
group: environment === 'production' ? 'appgroup' : process.env.USER
});
await service.enable();
await service.start();
}
🔧 Advanced Usage
Direct SystemdManager Access
For advanced users who need fine-grained control:
const daemon = new SmartDaemon();
// Access the SystemdManager directly
const systemdManager = daemon.systemdManager;
// Check if running on a compatible system
const canRun = await systemdManager.checkElegibility();
// Get all existing SmartDaemon services
const existingServices = await systemdManager.getServices();
// Reload systemd daemon
await systemdManager.reload();
Custom Service Templates
Access the template manager for custom service file generation:
const daemon = new SmartDaemon();
const template = daemon.templateManager.generateUnitFileForService(myService);
console.log(template); // View generated systemd unit file
🏗️ System Requirements
- Operating System: Linux with systemd (Ubuntu 16.04+, Debian 9+, CentOS 7+, RHEL 7+, Fedora, Arch, etc.)
- systemd 254 or later for
SystemdUnit.inspectServiceSettings(),SystemdUnit.cleanFileDescriptorStore(),runInTransientScope()and for installing a definition withfileDescriptorStorePreserve(Debian 13, Ubuntu 24.04, Fedora 39 and later ship it); an older manager is refused by name withmanager_too_old - Node.js: Version 14.x or higher
- Permissions: Either root access or properly configured sudo
⚙️ Generated Systemd Service Files
SmartDaemon generates professional systemd unit files with:
- Automatic restart on failure
- Network target dependencies
- Proper working directory configuration
- User/Group assignment
- Resource limits configuration
- Syslog integration for logging
- Environment variable support
Example generated service file:
[Unit]
Description=My API Server
Requires=network.target
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
Environment=NODE_OPTIONS="--max_old_space_size=100"
ExecStart=/bin/bash -c "cd /opt/myapp && node server.js"
WorkingDirectory=/opt/myapp
Restart=always
RestartSec=10
LimitNOFILE=infinity
LimitCORE=infinity
StandardOutput=syslog
StandardError=syslog
[Install]
WantedBy=multi-user.target
🐛 Troubleshooting
Permission Denied Errors
If you encounter "Interactive authentication required" errors:
-
Option 1: Run your application with sudo
sudo node your-app.js -
Option 2: Configure passwordless sudo (recommended for production)
-
Option 3: Provide sudo password programmatically
const daemon = new SmartDaemon({ sudoPassword: 'your-password' });
Service Not Found
If systemctl can't find your service:
- Check that the service was enabled:
await service.enable() - Verify the service name: Services are prefixed with
smartdaemon_ - Reload systemd:
await service.reload()
Service Fails to Start
Check logs using:
journalctl -u smartdaemon_your-service-name -f
🔒 Security Best Practices
- Never run services as root unless absolutely necessary
- Use dedicated service users with minimal permissions
- Configure passwordless sudo for production instead of storing passwords
- Limit sudo permissions to only the specific systemctl commands needed
- Regular security audits of your service configurations
🤝 Support
- 📧 Email: hello@task.vc
- 🐛 Issues: GitHub Issues
- 📖 Documentation: https://code.foss.global/push.rocks/smartdaemon
License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.