Skip to content

About

Real-time web dashboard for managing PM2 processes. Monitor CPU, memory, and uptime at a glance, stream live logs directly in the browser, restart processes with one click, and trigger custom PM2 actions, all over a single WebSocket connection.

Topics

Resources

Stars

23 stars

Watchers

0 watching

Forks

Repository files navigation

PM2-Hawkeye

Jetbrains Open Source

A modern, real-time web dashboard for your PM2 processes.

Monitor, restart, and tail logs - all from a sleek dark-mode UI.

Node.js License React WebSockets Tests


Features

  • Live process overview - CPU, memory, uptime, and restart count at a glance
  • Real-time log streaming - stdout & stderr tailed directly in the browser
  • Persistent monitoring - CPU/memory history and logs stored in SQLite, survives page reloads
  • One-click restart - restart any process with an inline confirmation
  • PM2 custom actions - trigger any axm_actions your processes expose
  • Application metrics - chart any value your app reports through tx2 or @pm2/io, with history for monitored processes
  • Runtime log level - switch a running process to debug without restarting it, reverts on restart
  • Secure by default - scrypt password hashing, CSRF protection, rate limiting, CSP headers
  • Single WebSocket connection - process list, live stats, log lines and deploy progress share one multiplexed stream; only stored history is refreshed over HTTP
  • Alerting - webhook and ntfy notifications when monitored processes log errors, with per-process mute and throttle support
  • Dark-mode UI - clean, responsive dashboard that works on desktop and mobile

How the dashboard is laid out

With nothing selected you see the Overview: a "Needs attention" list (errored processes, failed or waiting deployments, a host running out of CPU, RAM, or disk) followed by the host CPU, RAM, and disk charts. The Overview entry at the top of the sidebar, the logo, and the host readout in the top bar all lead back to it.

Pick a process in the sidebar, then work on it in one of its tabs. Every per-process function lives in exactly one of them.

Tab What it holds
Logs Merged stdout and stderr, level filters, search, pause, copy, download
Metrics CPU and memory history for the process (with host CPU for comparison) and its own tx2 / @pm2/io metrics
Deploy Only for processes PM2-Hawkeye deployed: the saved configuration, Redeploy now, Edit configuration, and the live progress of a running deploy
Manage Monitoring, per-process alerts, log level, custom actions, delete

Restart and Stop/Start sit in the process header, alongside a live strip of CPU, memory, restart count, and uptime that stays visible on every tab. The cpu and mem readouts there are shortcuts: clicking one opens the Metrics tab scrolled to that chart. They never draw a chart of their own, so every chart still lives in exactly one place. Hover a chart, or focus it and use the arrow keys, to read the exact values at any point in time.

Process charts show the last 180 stored samples, and a sample is only stored when its value changed, so a steady process reaches further back. The host charts cover the full retention window (METRICS_RETENTION_MS, 24 hours by default), averaged into five-minute buckets.


Screenshots

Login Main View
Screenshot showing the login page Screenshot showing the main view

📡 Live vs. Monitored Processes

PM2-Hawkeye has two modes for each process.

Live only (default)

When you open a process without enabling monitoring, you see real-time CPU, memory, and a live log stream. None of this is saved. Navigating away or closing the tab loses the history - when you return you start fresh.

Monitored

Click Turn on next to Store logs and metrics in the Manage tab (or Turn on monitoring in the Metrics tab) to enable persistent tracking. PM2-Hawkeye will:

  • Sample CPU and memory every 20 seconds and store the history (default: 24 hours)
  • Sample every application metric the process reports, on the same schedule and retention
  • Persist incoming log lines to the database (default: 14 days)
  • Chart CPU and memory over time in the Metrics tab
  • Backfill the last log entries from the PM2 log files on first enable

Monitoring state survives server restarts. You can turn monitoring off at any time - this removes all stored history for that process.


🚀 Quick Start

Prerequisites

  • Node.js 24 or higher
  • PM2 7 or newer, installed globally (npm i -g pm2@latest). After upgrading the global PM2, run pm2 update so the running daemon is replaced as well. Startup aborts with instructions when the pm2 command is missing, older than 7, or the running daemon is still on an older version
  • At least one PM2 process running

Important

Node.js only - PM2-Hawkeye must run on Node.js. Alternative runtimes such as Bun or Deno are not supported, because the native SQLite addon (better-sqlite3) crashes there with a segmentation fault. Keep in mind that bun run and bun start replace node inside package.json scripts with Bun itself, so bun start does not start the server on Node.js. Use npm start, or build with any package manager and start the server explicitly with node lib/transport/server.js. Startup aborts with an explanatory message when an unsupported runtime or a Node.js release older than 24 is detected.

1. Clone & install

git clone https://github.com/orangecoding/pm2-hawkeye.git
cd pm2-hawkeye
yarn install

2. Configure

Copy the example env file and open it in your editor:

cp .env.example .env

Generate a password hash and paste the output into .env:

node -e "
  const crypto = require('crypto');
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync('YOUR_PASSWORD', Buffer.from(salt,'hex'), 64).toString('hex');
  console.log('AUTH_PASSWORD_SALT=' + salt);
  console.log('AUTH_PASSWORD_HASH=' + hash);
"

3. Build & run

npm start

Open http://localhost:3030 in your browser.

4. Run as a PM2 process (recommended)

The recommended production setup is to run PM2-Hawkeye as a PM2 process itself so it is supervised and auto-restarted:

npm run build
pm2 start lib/transport/server.js --name pm2-hawkeye
pm2 save

🔄 Updating

update.sh handles the full update cycle for a self-hosted installation:

./update.sh

It will:

  1. Pull the latest commits from upstream (fast-forward only - aborts if there are local changes)
  2. Install or update Node.js dependencies
  3. Rebuild the frontend bundle
  4. Restart the pm2-hawkeye process under PM2 (if running)

Make the script executable once before first use:

chmod +x update.sh

If PM2-Hawkeye is not running under PM2 when the script finishes, it will print the commands to start it manually.

Note

Database location - by default the database is written to ./data/pm2-hawkeye.db inside the project directory. If you update by replacing the project folder (e.g. re-cloning), this file will be lost. Point SQLITE_DB_PATH at a directory outside the project to keep your data safe across updates (pm2-hawkeye.db is created inside it automatically):

SQLITE_DB_PATH=/var/lib/pm2-hawkeye

Create the directory first: mkdir -p /var/lib/pm2-hawkeye

Note

PM2 timestamps - PM2-Hawkeye merges stdout and stderr and sorts log lines chronologically. This requires PM2 to prefix each line with a timestamp. Always start your processes with --time:

pm2 start app.js --name my-app --time

Without it, log lines from stdout and stderr cannot be sorted correctly.


⚙️ Configuration

All settings are read from a .env file in the project root. Copy .env.example to get started. Set HAWKEYE_ENV_FILE to read (and, from the General settings page, write) a different file instead. Real environment variables take precedence over the file.

The file's values configure PM2-Hawkeye only: they are not exported into its process environment, so processes it starts or deploys through PM2 do not inherit its port, password hash or other settings.

Server

Variable Default Description
HOST 0.0.0.0 Bind address
PORT 3030 HTTP and WebSocket port

Authentication

Variable Default Description
AUTH_USERNAME admin Login username (case-insensitive)
AUTH_PASSWORD_SALT - Hex-encoded salt (generated above)
AUTH_PASSWORD_HASH - Hex-encoded scrypt hash (64 bytes)
SESSION_TTL_MS 28800000 Session lifetime in ms (default: 8 h)

Rate limiting

Variable Default Description
AUTH_MIN_RESPONSE_MS 900 Minimum login response time (timing-attack mitigation)
LOGIN_WINDOW_MS 600000 Sliding window for login attempts (10 min)
LOGIN_MAX_REQUESTS 12 Max login attempts per window
LOGIN_FAILURE_WINDOW_MS 1800000 Three failed logins within this window lock the client out (30 min)
LOGIN_MAX_LOCKOUT_MS 43200000 Lockout period after three failed logins (12 h)
UNAUTH_WINDOW_MS 60000 Window for unauthenticated access attempts (1 min)
UNAUTH_MAX_REQUESTS 10 Max unauthenticated hits before a 5 s penalty
UNAUTH_PENALTY_MS 5000 Delay added to rate-limited unauthenticated responses

Cookies & proxy

Variable Default Description
COOKIE_SECURE auto auto / always / never
TRUST_PROXY 0 Set to 1 when behind a reverse proxy

Behind a reverse proxy, set TRUST_PROXY=1 so the client IP (used for rate limiting), HTTPS detection (COOKIE_SECURE=auto) and the forwarded host come from the proxy's X-Forwarded-* headers. The proxy must pass WebSocket upgrades on /ws/ through and keep the original Host header or send X-Forwarded-Host: the WebSocket handshake is refused with 403 when its Origin does not match the host the browser used.

Storage

Variable Default Description
SQLITE_DB_PATH ./data Directory for the SQLite database (pm2-hawkeye.db is created inside). Relative paths resolve against the project directory
METRICS_RETENTION_MS 86400000 How long metric samples are kept (24 h)
LOGS_RETENTION_MS 1209600000 How long log entries are kept (14 days)
MAX_LOG_BYTES_PER_FILE 5242880 Max bytes read per PM2 log file (5 MB)

⚡ Custom Actions with tx2

PM2-Hawkeye can display and trigger custom actions that your app exposes to PM2 via tx2.

Install tx2

npm install tx2

Define actions in your app

import tx2 from 'tx2';

// Simple action
tx2.action('clear cache', (done) => {
  myCache.flush();
  done({ success: true });
});

// Action with a parameter
tx2.action('set sample rate', (rate, done) => {
  sampler.rate = Number(rate);
  done({ rate: sampler.rate });
});

For log levels specifically there is a dedicated control that needs no tx2, see Runtime log level below.

done() must always be called - it signals to PM2 that the action has completed and sends the return value back to the dashboard.

Trigger from PM2-Hawkeye

Once your process is running, open it in PM2-Hawkeye and go to the Manage tab. Any registered actions are listed there, each with its own Run button. Actions declared with arity: 1 ask for their parameter before running.

Available tx2 APIs

API Purpose
tx2.action(name, fn) Register a triggerable action
tx2.action(name, { arity: 1 }, fn) Action that accepts a parameter

For metrics, see the next section.


📈 Custom Metrics with tx2

Your app can hand numbers to PM2-Hawkeye and get a chart for each of them in the Metrics tab. There is no Hawkeye-specific API: the app reports metrics the standard PM2 way, through tx2 or @pm2/io, and PM2 keeps the latest value of each one in the process's axm_monitor data. PM2-Hawkeye reads that.

npm install tx2
import tx2 from 'tx2';

// A value read once per second
tx2.metric('queue size', () => queue.length);

// The same, with a unit shown next to the value
tx2.metric('cache hit rate', '%', () => cache.hitRate * 100);

// A value you set yourself (pass an object: tx2.metric('name') alone is rejected)
const users = tx2.metric({ name: 'active users' });
users.set(42);

// Counter: call .inc() / .dec()
const jobs = tx2.counter('jobs processed');
jobs.inc();

// Histogram: mean (or min, max, p95, ...) of recorded values
const latency = tx2.histogram({ name: 'db latency', unit: 'ms', measurement: 'p95' });
latency.update(12);

// Meter: events per second
const requests = tx2.meter({ name: 'requests', unit: 'req/sec' });
requests.mark();

How it shows up

  • The Metrics tab gets an Application metrics group with one chart per metric, sorted by name. The current value updates live every three seconds.
  • History is stored only for monitored processes: every 20 seconds, kept as long as CPU and memory history (default: 24 hours). Without monitoring you see the live value only.
  • Only numeric values are charted. Numeric strings ('35.25') are accepted; anything else is skipped. At most 50 metrics per process are read.
  • PM2 injects @pm2/io into every Node.js process it starts (unless pmx: false), so even an app that registers nothing shows its runtime metrics: heap usage, event loop latency, active handles and requests, and HTTP latency once it serves requests.
  • PM2 clears the metrics of a process when it restarts. A metric that is no longer reported keeps its stored history until the retention window drops it.

Runtime log level

The Manage tab can raise or lower the log level of a running process without restarting it. The level applies to that instance only: as soon as the process restarts it is back to whatever the application boots with.

Node has no log level that can be changed from outside a process, so the application has to listen. PM2 opens an IPC channel to every Node process it starts, so this needs no dependency:

process.on('message', (packet) => {
  if (packet?.topic !== 'hawkeye:log-level') return;
  if (packet.data?.level) logger.level = packet.data.level;
  process.send({
    type: 'hawkeye:log-level:ack',
    data: { level: logger.level },
  });
});

The answer is identified by its type, not by a topic: PM2 delivers topic intact on the way in but strips it from anything a process sends back.

A message with data.level sets the level. A message without one only answers, which is how the dashboard reads the current level without changing anything. Always answer with the level the logger actually ended up on, not the one that was requested, so the dashboard shows the truth. A process that does not answer within 1.5 s is reported in the UI as not supporting this.

Accepted levels: trace, debug, info, warn, error, fatal, silent.

This works with any logger that can change its level at runtime. It is tested with pino, where three details are worth knowing:

  • logger.level = 'debug' takes effect immediately, and child loggers without a level of their own follow the root, including ones created earlier.
  • A child created with an explicit { level: 'warn' } stays at warn.
  • pino-pretty only formats and does not filter, but a transport configured with its own level is a second gate and will still drop lines.

One display note: the Logs tab detects a line's level from its text. Pretty printed output (DEBUG (1234): ...) is recognised, raw pino JSON is not, because it writes levels as numbers ("level":20). Those lines still show, they just carry no level badge and are unaffected by the level filters.

Processes with no IPC channel, so anything not started as a Node process by PM2, cannot be controlled this way and will report as unsupported.


Development

PM2-Hawkeye uses a two-process dev setup: the Node.js backend runs on port 3030 and an esbuild dev server handles the frontend on port 3042 with instant rebuilds and hot module replacement.

Start the backend

node --inspect lib/transport/server.js

Start the frontend dev server

npm run dev

Open http://localhost:3042. The dev server proxies all /api/*, /ws/*, and HTML routes through to the backend.

Tests, lint, format

npm test
npm run lint
npm run format        # write
npm run format:check  # check only

🔔 Alerting

PM2-Hawkeye can send you a notification whenever a monitored process logs something that matches your configured severity threshold - for example, every time an error is written to stderr.

How it works

Log lines captured from your PM2 processes flow through a detection pipeline:

  1. Level detection - each line is inspected for a severity prefix (ERROR, WARN, INFO, DEBUG). Lines arriving on stderr that carry no detectable prefix are automatically treated as error.
  2. Threshold check - the detected level is compared against the set of levels you selected in Settings. If the level is not in the set, the line is silently discarded.
  3. Per-process mute check - if alerts are switched off for that process in its Manage tab, the alert is skipped.
  4. Throttle check - in throttle mode a second alert for the same process is suppressed until the configured cool-down window has elapsed. In every match mode all qualifying lines trigger a notification.
  5. Dispatch - all enabled reporters are called concurrently. A failure in one reporter does not block the others.

Settings UI

Click the gear icon in the top bar to open the settings overlay.

  • General - your .env values, grouped by concern (server, sign in, brute-force protection, retention, deployments) with the raw key shown under each field. Also changes your login password. Changes are written to disk; a restart of PM2-Hawkeye is required for them to take effect.
  • Alerting - configure the alert mode, log level thresholds, and reporters.

Reporters

Webhook

Sends an HTTP POST request to any URL you choose. You can attach custom request headers and build a fully custom JSON body using key/value pairs with template variables:

Variable Replaced with
{logLevel} Detected log level (error, warn, info, debug)
{log_message} The full log line text
{process_name} The PM2 process name

Variable substitution is JSON-aware: inside a quoted JSON string the value is escaped (quotes, line breaks), and as an unquoted JSON value it becomes a complete JSON string, so log lines never produce malformed payloads. A value that is not JSON at all is sent as plain text. Line breaks in header values are replaced by spaces.

A live curl preview updates in real time as you fill in the form so you can verify the exact request before saving.

ntfy

Sends a push notification to an ntfy topic. Configure the server URL, topic, message priority, and an optional auth token. PM2-Hawkeye sets the Title, Priority, and Tags headers automatically.

Per-process mute

Open a monitored process and go to its Manage tab. The Alerts switch under History and alerts mutes and unmutes notifications for that process alone. The preference is stored in the database and survives restarts.


Deploy

PM2-Hawkeye can clone, install, build, and register new Node.js applications directly from the UI -- no manual SSH required.

How it works

Click New deployment in the top bar to open the deploy form. Four fields are required (repository URL, branch, app name, start script), and the app name fills itself in from the repository URL until you edit it. Everything else is folded into collapsible option groups that show their current value on the summary line. Next to the form, a preview lists the exact steps and commands the deployment will run, updated as you type. Hit Deploy and PM2-Hawkeye will:

  1. Run an optional pre-setup shell script (e.g. install system dependencies)
  2. git clone the repository into DEPLOY_BASE_DIR/<app-name> (default: ./apps/<app-name>)
  3. Run the install command (npm install, yarn, or pnpm install)
  4. Run an optional build command (e.g. npm run build)
  5. Run an optional post-setup shell script (e.g. run database migrations)
  6. Register and start the process with PM2

The form closes once the deployment has started, and the new app's page shows each step live in its Deploy tab, together with the output of every command. You can switch to another process meanwhile; the deployment keeps running on the server even if you close the page.

Deploy form fields

Where the code comes from (required)

Field Description
Repository URL HTTPS URL of a GitHub or GitLab repository, or an SSH URL such as [email protected]:user/repo.git. A private repository needs a deploy key on the server.
Branch Git branch to clone and deploy from. Defaults to master.
App name Unique PM2 process name. Alphanumeric, dashes, and underscores only. It also names the deploy folder, so it cannot change after the first deploy.

How to build and start it

Field Description
Install command Package manager command. Select Skip installing to skip dependency installation entirely.
Extra install flags Appended to the install command, e.g. --omit=dev.
Build command Optional build step, e.g. npm run build. Leave empty to skip it.
Start script Entry point relative to the repo root, e.g. src/server.js. Required.

Options (optional, collapsed by default)

Group Field Description
Environment Env file Path to a .env file inside the repo to load at start time, e.g. .env.production.
Environment Variables Key/value pairs passed directly to the process. These override values from the env file.
Auto-deploy Deploy automatically Poll the branch and redeploy whenever it gets new commits. Off by default.
Auto-deploy Check every Minutes between polls, 1 to 1440. Defaults to 5.
Local changes on redeploy Discard local changes without asking When a redeploy finds modified tracked files in the deploy directory, run git reset --hard straight away instead of pausing to ask. Untracked files are kept. Off by default.
Setup scripts Pre-setup script Shell script that runs in DEPLOY_BASE_DIR before cloning. Use it to check requirements or install OS packages.
Setup scripts Post-setup script Shell script that runs in the cloned repo directory after the build. Use it for migrations, file permission fixes, etc.
How PM2 runs it Interpreter Node.js interpreter path. Leave as node for the system default.
How PM2 runs it Exec mode fork (single process) or cluster (multiple workers).
How PM2 runs it Instances Number of cluster workers. Set -1 to use all CPU cores.

Two further groups cover crash recovery (auto-restart, memory limits, scheduled restarts, ready signals) and logging and file watching.

Redeploying and editing

A process that PM2-Hawkeye deployed is marked as such in two places: a git icon next to its name in the sidebar, and a chip in its header showing the repository and branch it came from. Such a process gets a Deploy tab, and the chip opens it.

The Deploy tab shows the saved configuration (repository, branch, folder on the server, last successful deploy), the steps a redeploy runs, the auto-deploy state, and the names of the configured environment variables (never their values). From there:

  • Redeploy now runs the saved configuration straight away, after a one-click confirmation and without opening a form. It fetches the configured branch from the configured repository (git fetch plus git checkout -B), then reinstalls, rebuilds, and restarts the PM2 process. Commits made inside the deploy directory are dropped; untracked files such as a .env are kept.
  • Edit configuration opens the deploy form pre-filled. Save only stores the configuration without touching the running process; Save and redeploy stores it and redeploys at once. Changing the repository URL or the branch therefore takes effect on the next redeploy.

While a deploy runs, the Deploy tab shows its progress: each step with its state, and the live output of every command. The run stays on screen once it has finished, successful or not, until you dismiss it; a failed run offers Try again and a shortcut to the configuration. If the deploy directory has uncommitted changes to tracked files, the redeploy pauses there and asks whether to discard them or cancel; turn on Discard local changes without asking under Local changes on redeploy to skip that question for this deployment. Progress is kept in the browser only, so after a page reload a running deploy shows its new output but not what came before.

Deployments without a PM2 process are listed under Not running in the sidebar, tagged Deploy failed (the first deploy never finished), Deploying, or Not in PM2 (it ran before, but the process was deleted from PM2). Selecting one opens its own page with Deploy again, Edit configuration, and Delete deployment.

Auto-deploy on new commits

Every deployment can watch its own branch. Enable Auto-deploy in the deploy form and pick a poll interval; PM2-Hawkeye then runs git ls-remote against the repository on that interval and compares the branch head with the commit checked out on disk. When the branch has moved ahead, it runs exactly the same sequence as a manual redeploy: fetch and checkout, install, build, post-setup script, PM2 restart. Progress streams over the same WebSocket, so an auto-deploy is visible live if you happen to have the app open.

Three details worth knowing:

  • Local changes are discarded. A manual redeploy asks before throwing away uncommitted tracked changes in the deploy directory. An auto-deploy has nobody to ask, so it runs git reset --hard and continues. Do not edit deployed files in place on a watched deployment.
  • One deploy per commit. The SHA that triggered a deploy is recorded before the deploy starts, so a failing deploy waits for the next commit instead of retrying every interval.
  • Results are notified. If a webhook or ntfy reporter is enabled under Settings - Alerting, each auto-deploy sends one notification with its outcome. Unlike log alerts, this ignores the log-level threshold, the throttle window, and the per-process alerts toggle.

The Manage tab shows the current state under the repository URL: the interval, when the branch was last checked, and the last error if a poll or an auto-deploy failed. Polling uses whatever git credentials the server already has (including GIT_SSH_COMMAND or a core.sshCommand pointing at a deploy key), with prompts disabled, so a private repository without a usable key fails fast and records the error instead of hanging.

Configuration

Set DEPLOY_BASE_DIR in your .env to control where apps are cloned:

DEPLOY_BASE_DIR=./apps

The directory is created automatically on first deploy. Use an absolute path to place apps outside the project directory.


Security

  • CSRF tokens - one-time-use tokens rotated after every state-changing request
  • Rate limiting - sliding window plus a lockout after three failed logins; separate rate limit for unauthenticated access
  • CSP headers - strict Content-Security-Policy including ws:/wss: for WebSockets
  • Timing-attack mitigation - constant-time credential comparison with a minimum response delay
  • Secure cookies - HttpOnly, SameSite=Strict, optional Secure flag

Sponsorship

I maintain this and other open-source projects in my free time. If you find it useful, consider supporting the project.


License

Apache-2.0 - © Christian Kellner

About

Real-time web dashboard for managing PM2 processes. Monitor CPU, memory, and uptime at a glance, stream live logs directly in the browser, restart processes with one click, and trigger custom PM2 actions, all over a single WebSocket connection.

Topics

Resources

Stars

23 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages