tutorial

How to Monitor Your Self-Hosted OrangeHRM Instance (and Get Alerted Before HR Grinds to a Halt)

OrangeHRM downtime hits harder than most app outages — payroll stalls, leave requests queue up, and onboarding breaks. Here's how to add uptime monitoring, heartbeats for critical HR workflows, and instant alerts in under 30 minutes.

How to Monitor Your Self-Hosted OrangeHRM Instance (and Get Alerted Before HR Grinds to a Halt)

OrangeHRM downtime isn't just an inconvenience — it's a compliance risk. When your self-hosted HR system goes dark, leave requests pile up unanswered, payroll jobs miss their processing windows, and new employee onboarding grinds to a halt. By the time someone notices, you're dealing with data integrity questions on top of an outage.

This guide walks you through setting up complete monitoring for a self-hosted OrangeHRM deployment: HTTP availability checks, PHP-FPM process health, MySQL connectivity, and heartbeat monitoring for critical background workflows — all wired to instant alerts.


What breaks silently in OrangeHRM

OrangeHRM runs on PHP/Symfony behind a web server (typically Apache or Nginx), backed by MySQL and a cron-driven task scheduler. When things go wrong, they tend to go wrong quietly:

Web availability failures — the application returns 500 errors or a blank page. PHP-FPM crashes, Symfony caches go stale, or a misconfigured .htaccess block. Users see nothing useful; your monitoring should catch it first.

PHP-FPM pool exhaustion — under load, PHP-FPM worker pools fill up. New requests queue or get dropped. The application appears intermittently slow or broken with no obvious error.

MySQL connectivity issues — OrangeHRM's entire data layer sits on MySQL. Connection pool exhaustion, slow queries, or a replication lag on replica nodes causes transaction failures that look like application bugs.

Silent cron failures — leave carryover calculations, payroll batch jobs, and email notification dispatches all run via cron. If cron stops firing, those processes silently stop working. No exception is raised; nothing alerts you.


Step 1: Create a health check endpoint

OrangeHRM doesn't ship a /health endpoint, but you can add one that checks what actually matters: PHP responsiveness and database connectivity.

Create a standalone health check file outside OrangeHRM's Symfony routing layer for simplicity:

<?php
// /var/www/orangehrm/public/health.php

header('Content-Type: application/json');

$checks = [];
$status = 200;

// Database connectivity check
try {
    $dsn = 'mysql:host=' . ($_ENV['ORANGEHRM_DB_HOST'] ?? 'localhost') .
           ';dbname=' . ($_ENV['ORANGEHRM_DATABASE_NAME'] ?? 'orangehrm');
    $pdo = new PDO(
        $dsn,
        $_ENV['ORANGEHRM_DB_USERNAME'] ?? 'orangehrm',
        $_ENV['ORANGEHRM_DB_PASSWORD'] ?? '',
        [PDO::ATTR_TIMEOUT => 3]
    );
    $pdo->query('SELECT 1');
    $checks['database'] = 'ok';
} catch (Exception $e) {
    $checks['database'] = 'error';
    $status = 500;
}

// Check that OrangeHRM's cache directory is writable
$cacheDir = __DIR__ . '/../cache';
$checks['cache_writable'] = is_writable($cacheDir) ? 'ok' : 'error';
if ($checks['cache_writable'] === 'error') {
    $status = 500;
}

http_response_code($status);
echo json_encode([
    'status' => $status === 200 ? 'healthy' : 'degraded',
    'checks' => $checks,
    'timestamp' => date('c'),
]);

Protect this file so it's only accessible from your monitoring service's IP:

# In your Nginx server block
location = /health.php {
    allow 203.0.113.10;  # Vigilmon probe IP range
    deny all;
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Test it:

curl -i http://localhost/health.php
# HTTP/1.1 200 OK
# {"status":"healthy","checks":{"database":"ok","cache_writable":"ok"},"timestamp":"2026-01-01T00:00:00+00:00"}

Step 2: Set up HTTP uptime monitoring in Vigilmon

With the health endpoint live, point Vigilmon at it:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://hrm.yourdomain.com/health.php
  4. Set check interval to 5 minutes
  5. Add an assertion: response body contains "status":"healthy"
  6. Save

Add separate monitors for the core application URLs:

| Monitor name | URL | Why | |---|---|---| | OrangeHRM Login | https://hrm.yourdomain.com/auth/login | Auth layer is reachable | | OrangeHRM Dashboard | https://hrm.yourdomain.com/dashboard | Main app is rendering | | OrangeHRM API | https://hrm.yourdomain.com/api/v2/leave/leave-types | REST API is responding |

Each monitor runs independently. An API failure with a healthy homepage tells you exactly where the break is.


Step 3: Monitor PHP-FPM pool health

PHP-FPM has a built-in status endpoint that exposes pool metrics. Enable it:

; /etc/php/8.1/fpm/pool.d/www.conf
pm.status_path = /fpm-status

Expose it via Nginx (internal access only):

location = /fpm-status {
    allow 127.0.0.1;
    allow 203.0.113.10;  # Vigilmon probe
    deny all;
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Reload PHP-FPM:

systemctl reload php8.1-fpm
curl http://localhost/fpm-status
# pool:                 www
# process manager:      dynamic
# start time:           ...
# active processes:     2
# idle processes:       3
# max active processes: 8

Create an HTTP monitor in Vigilmon for http://hrm.yourdomain.com/fpm-status checking that the response is 200. If PHP-FPM crashes, this endpoint stops responding and Vigilmon alerts you immediately.


Step 4: Heartbeat monitoring for critical HR workflows

HTTP monitoring catches server failures. But payroll batch jobs, leave carryover calculations, and email notifications all run on cron — and cron failures are invisible without heartbeats.

The heartbeat pattern: your cron job pings a URL at the end of every successful run. If Vigilmon stops seeing pings within the expected window, it alerts you.

Leave management workflow heartbeat

#!/bin/bash
# /opt/orangehrm-jobs/process-leave.sh

cd /var/www/orangehrm

# Run OrangeHRM's leave processing
php bin/console orangehrm:leave:generate-emp-leave-request --env=prod

if [ $? -eq 0 ]; then
    curl -fsS --retry 3 "${VIGILMON_LEAVE_HEARTBEAT_URL}" > /dev/null 2>&1
    echo "[$(date)] Leave processing completed and heartbeat sent"
else
    echo "[$(date)] Leave processing FAILED — heartbeat NOT sent" >&2
fi

Payroll processing heartbeat

#!/bin/bash
# /opt/orangehrm-jobs/process-payroll.sh

cd /var/www/orangehrm

php bin/console orangehrm:payroll:process --env=prod

if [ $? -eq 0 ]; then
    curl -fsS --retry 3 "${VIGILMON_PAYROLL_HEARTBEAT_URL}" > /dev/null 2>&1
fi

Email notification delivery heartbeat

<?php
// /opt/orangehrm-jobs/send-notifications.php
// Run via cron to dispatch queued email notifications

require_once '/var/www/orangehrm/vendor/autoload.php';

// Process queued notifications...
$dispatcher = new OrangeHRM\Core\Notification\NotificationDispatcher();
$sent = $dispatcher->dispatchPending();

if ($sent !== false) {
    $heartbeatUrl = getenv('VIGILMON_EMAIL_HEARTBEAT_URL');
    if ($heartbeatUrl) {
        $ch = curl_init($heartbeatUrl);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        curl_close($ch);
    }
}

Crontab configuration

# /etc/cron.d/orangehrm-monitors

# Leave processing — daily at 1 AM
0 1 * * * orangehrm /opt/orangehrm-jobs/process-leave.sh >> /var/log/orangehrm/leave.log 2>&1

# Payroll processing — 1st of each month at 2 AM
0 2 1 * * orangehrm /opt/orangehrm-jobs/process-payroll.sh >> /var/log/orangehrm/payroll.log 2>&1

# Email notifications — every 15 minutes
*/15 * * * * orangehrm php /opt/orangehrm-jobs/send-notifications.php >> /var/log/orangehrm/notifications.log 2>&1

# Report generation — every hour
0 * * * * orangehrm php /var/www/orangehrm/bin/console orangehrm:report:generate --env=prod && curl -fsS "$VIGILMON_REPORTS_HEARTBEAT_URL" > /dev/null

Set environment variables in /etc/environment or in the cron environment:

VIGILMON_LEAVE_HEARTBEAT_URL=https://vigilmon.online/heartbeats/your-leave-token
VIGILMON_PAYROLL_HEARTBEAT_URL=https://vigilmon.online/heartbeats/your-payroll-token
VIGILMON_EMAIL_HEARTBEAT_URL=https://vigilmon.online/heartbeats/your-email-token
VIGILMON_REPORTS_HEARTBEAT_URL=https://vigilmon.online/heartbeats/your-reports-token

In Vigilmon, create a Heartbeat Monitor for each job:

  1. Click New Monitor → Heartbeat
  2. Name it (e.g. "OrangeHRM Payroll Job")
  3. Set the expected interval (match your cron schedule — payroll monthly means 32 days to be safe)
  4. Copy the ping URL and set it in your environment
  5. Save

Step 5: Employee onboarding pipeline monitoring

The onboarding pipeline in OrangeHRM involves multiple sequential steps — creating the employee record, setting up access, triggering welcome emails, and assigning to departments. Create a lightweight check that validates the pipeline is functional:

<?php
// /var/www/orangehrm/public/onboarding-health.php

header('Content-Type: application/json');

try {
    $dsn = 'mysql:host=localhost;dbname=orangehrm';
    $pdo = new PDO($dsn, $_ENV['ORANGEHRM_DB_USERNAME'], $_ENV['ORANGEHRM_DB_PASSWORD']);

    // Check for any stuck onboarding tasks (pending > 24 hours)
    $stmt = $pdo->query("
        SELECT COUNT(*) as stuck_count
        FROM ohrm_employee_work_shift
        WHERE created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)
        AND status = 'pending'
    ");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    $status = ($row['stuck_count'] == 0) ? 'ok' : 'degraded';
    http_response_code($status === 'ok' ? 200 : 500);
    echo json_encode(['onboarding_pipeline' => $status, 'stuck_tasks' => $row['stuck_count']]);
} catch (Exception $e) {
    http_response_code(500);
    echo json_encode(['onboarding_pipeline' => 'error']);
}

Add this as an HTTP monitor in Vigilmon checking the response body for "onboarding_pipeline":"ok".


Step 6: Alerts via Slack or email

Configure notifications in Vigilmon so alerts reach whoever is on call:

Slack:

  1. Create an incoming webhook in your Slack workspace
  2. Go to Notifications → New Channel → Slack in Vigilmon
  3. Paste the webhook URL and test it
  4. Enable it on all OrangeHRM monitors

Email:

  1. Go to Notifications → New Channel → Email
  2. Add your HR team email or a dedicated ops distribution list
  3. Enable on critical monitors (web availability, database, payroll heartbeat)

A payroll heartbeat miss alert looks like:

🔴 MISSED: OrangeHRM Payroll Job
Expected every: 32 days
Last successful ping: 34 days ago
Monitor: OrangeHRM Payroll Job

Step 7: Status page for your HR team

A shared status page lets your HR team check system health without filing an IT ticket every time something feels slow:

  1. Go to Status Pages → New Status Page
  2. Name it "HR Systems Status"
  3. Add your OrangeHRM monitors
  4. Share the URL with your HR team and link it from your intranet

What you've built

| Component | Monitoring approach | |---|---| | Web application availability | HTTP monitor on /health.php | | PHP-FPM pool health | HTTP monitor on /fpm-status | | MySQL connectivity | Database check inside health endpoint | | Leave management workflow | Heartbeat — daily cron job | | Payroll processing | Heartbeat — monthly batch job | | Email notification delivery | Heartbeat — every 15 minutes | | Report generation | Heartbeat — hourly job | | Onboarding pipeline | HTTP monitor on /onboarding-health.php |

The full setup runs on Vigilmon's free tier and takes less time than diagnosing a payroll job that silently failed a week ago.


Next steps

  • Add a monitor for your OrangeHRM backup job — data loss during an incident is worse than the incident itself
  • Set up response time tracking to catch gradual slowdowns before they become outages
  • Monitor your MySQL slow query log volume as a leading indicator of database stress

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →