unitup is a zero-dependency CLI & library to run Node.js, Python, Ruby, PHP, Bun, Deno, Go, Elixir, Shell scripts, and compiled native executables as native OS systemd user services without requiring sudo.
npm install -g unitup
# Add & start Node.js server
unitup add server.js --start
unitup status server
unitup logs server --follow
# Multi-runtime & Native executables
unitup add worker.py --runtime python --start
unitup add ./server --runtime native --name api --start
Exits immediately after configuring services. Zero persistent memory or CPU overhead from unitup itself.
Out-of-the-box support for Node.js, Python, Ruby, PHP, Bun, Deno, Go, Elixir, Shell, and native binaries.
Avoids shell execution (shell: false). Saves standard text unit files directly to ~/.config/systemd/user/.
Resolves absolute binary & script paths and automatically injects proper PATH environment into unit files.
Built strictly with Node.js standard library modules. Extremely lightweight ESM & CJS footprint.
Full support for existing Node.js configurations and legacy metadata formats without breaking changes.
unitup is not a PM2 replacement or an independent process manager. While PM2 runs its own master daemon, unitup is strictly a thin, transparent CLI layer on top of Linux OS systemd.
| Feature | unitup | PM2 |
|---|---|---|
| Background Process (Daemon) | No resident daemon. Systemd directly supervises processes. | Runs a persistent process-management daemon. |
| System Integration | Native Linux OS systemd user service (~/.config/systemd/user/). |
Custom process monitoring via PM2's internal daemon. |
| Multi-Runtime Support | Node, Python, Ruby, PHP, Bun, Deno, Go, Elixir, Shell, Native Binaries. | Node.js-focused process manager with support for running other commands. |
| Privileges Required | Does not require sudo for normal service creation and management. |
Boot integration commonly requires running PM2 startup with elevated privileges. |
| Dependencies | 0 Runtime Dependencies (Node.js standard modules only). | Dozens of 3rd party npm packages. |
| Log Management | Delegates 100% to native Linux journald system (journalctl). |
Manages .pm2/logs files (requires pm2-logrotate plugin). |
| Boot Startup | Native systemd lingering (loginctl enable-linger). |
Custom startup script launching PM2 master daemon. |
At its core, unitup generates systemd service unit files directly from a generic command + args model rather than being hardcoded to Node.js:
{
"command": "/usr/bin/python3",
"args": ["/home/user/apps/worker.py"],
"cwd": "/home/user/apps",
"env": {
"APP_ENV": "production"
}
}
[Unit]
Description=unitup service: worker
After=network.target
[Service]
Type=simple
SyslogIdentifier=unitup-worker
WorkingDirectory=/home/user/apps
ExecStart=/usr/bin/python3 /home/user/apps/worker.py
Restart=on-failure
RestartSec=3
Environment=PATH="/usr/bin:/usr/local/bin:/bin"
Environment=APP_ENV="production"
[Install]
WantedBy=default.target
| Runtime | Extension / Auto-Detection | Command Executed |
|---|---|---|
| Node.js | .js, .mjs, .cjs or shebang #!/usr/bin/env node |
node <script> |
| Python | .py or shebang #!/usr/bin/env python3 |
python3 <script> |
| Ruby | .rb or shebang #!/usr/bin/env ruby |
ruby <script> |
| PHP | .php or shebang #!/usr/bin/env php |
php <script> (Supports -S 0.0.0.0:8080) |
| Bun | Explicit --runtime bun or shebang #!/usr/bin/env bun |
bun <script> |
| Deno | Explicit --runtime deno or shebang #!/usr/bin/env deno |
deno run <script> |
| Go | .go or shebang #!/usr/bin/env go |
go run <script> |
| Elixir | .ex, .exs or shebang #!/usr/bin/env elixir |
elixir <script> |
| Shell | .sh or shebang #!/bin/bash / #!/bin/sh |
bash <script> |
| Native Executable | Explicit --runtime native |
Direct binary execution (./server) |
unitup add ./server --runtime native) is recommended instead of go run.
unitup doctorRuns system readiness diagnostics (Linux OS, systemd PID 1, user bus, lingering, unit directory permissions, and detected runtimes).
unitup doctor
unitup add <script|exec> [options]Generates and registers a new systemd user service unit file.
# Flags:
# --name Service name (default: script basename)
# --runtime Specify runtime (node, python, ruby, php, bun, deno, shell, go, elixir, native)
# --runtime-arg Argument passed to binary (e.g. -S 0.0.0.0:8080 or --allow-net)
# --command Explicit binary executable (bypasses auto-detection)
# --arg Argument to pass to command
# --group Assign service to group (default: default)
# --env KEY=val Set environment variable
# --env-file Load environment file
# --start Enable and start service immediately
# --force, -f Force overwrite of currently running service
unitup add worker.py --name worker --group backend --env APP_ENV=production --start
unitup list / unitup lsLists all managed services in a clean aligned table.
unitup list --group backend
unitup inspect <name>Displays full service configuration overview (Command, Arguments, Working directory, Unit file) without revealing secrets or environment variables.
unitup inspect worker
unitup start / stop / restart <name|@group>Controls service state individually or in batch using @group syntax.
unitup start @backend --enable
unitup restart worker
unitup stop @backend
unitup limits <name> [options]Configures native systemd memory limits for a service or resets limits back to system defaults.
unitup limits api --memory-high 400M --memory-max 512M --swap-max 256M
unitup limits api --reset-memory
unitup status / logs <name>Inspects process status with live memory metrics or streams journalctl logs with advanced filtering.
unitup status worker
unitup logs worker --follow --since 1h --priority err --json
unitup journal <action> [options]Performs safe journalctl storage maintenance (disk-usage, rotate, vacuum by size, time, or file count).
unitup journal disk-usage
unitup journal rotate
unitup journal vacuum --size 500M --yes
unitup journal vacuum --time 14d --dry-run
unitup remove <name|@group> [--force]Stops, disables, and deletes a service or group. Active running services require --force / -f to prevent accidental deletion.
unitup remove worker
unitup remove @backend --force
unitup exports full ESM and CommonJS interfaces with TypeScript declarations (index.d.ts).
import {
createService,
startService,
stopService,
restartService,
getServiceStatus,
listServices,
isSystemdAvailable
} from "unitup";
import type {
CreateServiceOptions,
ServiceStatus
} from "unitup";
if (await isSystemdAvailable()) {
// Add service with explicit command + args
await createService({
name: "worker",
command: "/usr/bin/python3",
args: ["/home/user/apps/worker.py"],
cwd: "/home/user/apps",
restart: "on-failure",
start: true
});
const status: ServiceStatus = await getServiceStatus("worker");
console.log(`PID: ${status.pid}, Status: ${status.status}`);
}
const {
createService,
startService,
getServiceStatus
} = require("unitup");
await createService({
name: "api",
script: "./server.js",
start: true
});
Define your entire application stack declaratively using unitup.yml or unitup.json:
unitup.yml Specapps:
api:
runtime: node
script: ./server.js
env:
NODE_ENV: production
PORT: 3000
start: true
worker:
runtime: python
script: ./worker.py
env:
APP_ENV: production
native-service:
runtime: native
command: ./server
# Preview differences against active units
unitup diff
# Apply unitup.yml configuration
unitup apply
# Perform batch operations on group
unitup restart @default
*Note: Relative script, command, cwd, and env-file paths in unitup.yml are resolved relative to the configuration file.
Depending on the Linux distribution and systemd-logind configuration, user services may stop after logout. Enable lingering to allow them to continue running without an active login session:
loginctl enable-linger $USER
Verify lingering state anytime with unitup doctor. unitup will warn if lingering is disabled but will never prompt for sudo automatically.