Sometimes you don’t need a full monitoring stack. You don’t need Prometheus, Grafana, or even Uptime Kuma. You just want to SSH into your server and see if everything’s okay in two seconds.
That’s exactly what this script does. One file, zero dependencies, instant health report.
What It Checks
| Check | What You See |
|---|---|
| 🖥 System | Uptime and CPU load average |
| 🧠 Memory | Usage percentage with warnings |
| 💾 Disk | All mounted filesystems |
| 🐳 Docker | Every container — running or dead |
| 🔌 Ports | What’s listening and which process owns it |
| 🔒 SSL | Certificate expiry for your domains |
Everything is color-coded: green means good, yellow means warning, red means fix it now.
The Script
Create a file called stackcheck.js on your server:
#!/usr/bin/env node
const { execSync } = require('child_process');
const { connect } = require('tls');
const os = require('os');
const WARN = '\x1b[33m⚠\x1b[0m';
const OK = '\x1b[32m✓\x1b[0m';
const FAIL = '\x1b[31m✗\x1b[0m';
const BOLD = '\x1b[1m';
const RESET = '\x1b[0m';
function run(cmd) {
try { return execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim(); }
catch { return null; }
}
function header(title) { console.log(`\n${BOLD}── ${title} ──${RESET}`); }
function checkUptime() {
header('System');
const up = os.uptime();
const days = Math.floor(up / 86400);
const hours = Math.floor((up % 86400) / 3600);
const load = os.loadavg();
const cpus = os.cpus().length;
const icon = load[0] > cpus ? FAIL : load[0] > cpus * 0.7 ? WARN : OK;
console.log(` ${OK} Uptime: ${days}d ${hours}h`);
console.log(` ${icon} Load: ${load.map(l => l.toFixed(2)).join(', ')} (${cpus} cores)`);
}
function checkMemory() {
header('Memory');
const total = os.totalmem();
const free = os.freemem();
const used = total - free;
const pct = Math.round(used / total * 100);
const icon = pct >= 90 ? FAIL : pct >= 75 ? WARN : OK;
const gb = n => (n / 1073741824).toFixed(1);
console.log(` ${icon} ${gb(used)}G / ${gb(total)}G (${pct}%)`);
}
function checkDisk() {
header('Disk');
const out = run("df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs -x overlay 2>/dev/null || df -h");
if (!out) return console.log(` ${WARN} Could not check disk`);
for (const [i, line] of out.split('\n').entries()) {
if (i === 0) { console.log(` ${line}`); continue; }
const pct = parseInt((line.match(/(\d+)%/) || [])[1]);
console.log(` ${pct >= 90 ? FAIL : pct >= 75 ? WARN : OK} ${line}`);
}
}
function checkDocker() {
header('Docker');
const out = run('docker ps -a --format "{{.Names}}\\t{{.Status}}\\t{{.Image}}"');
if (!out) return console.log(` ${WARN} Docker not available`);
let up = 0, down = 0;
for (const line of out.split('\n')) {
const [name, status, image] = line.split('\t');
const running = status.toLowerCase().startsWith('up');
running ? up++ : down++;
console.log(` ${running ? OK : FAIL} ${name.padEnd(30)} ${status}`);
}
console.log(`\n Up: ${up} | Down: ${down}`);
}
function checkPorts() {
header('Ports');
const out = run("ss -tlnp 2>/dev/null | tail -n +2") || run("netstat -tlnp 2>/dev/null | tail -n +2");
if (!out) return console.log(` ${WARN} Could not check ports`);
const ports = new Map();
for (const line of out.split('\n')) {
const m = line.match(/:(\d+)\s/);
const proc = (line.match(/users:\(\("([^"]+)"/) || [])[1] || '';
if (m) ports.set(m[1], proc);
}
for (const [port, proc] of [...ports].sort((a, b) => a[0] - b[0])) {
console.log(` ${OK} :${port.padEnd(8)} ${proc}`);
}
}
async function checkSSL(domains) {
if (!domains.length) return;
header('SSL Certificates');
for (const domain of domains) {
await new Promise(resolve => {
const sock = connect(443, domain, { servername: domain, timeout: 5000 }, () => {
const cert = sock.getPeerCertificate();
sock.end();
const days = Math.round((new Date(cert.valid_to) - Date.now()) / 86400000);
const icon = days <= 7 ? FAIL : days <= 30 ? WARN : OK;
console.log(` ${icon} ${domain.padEnd(30)} ${days} days left`);
resolve();
});
sock.on('error', () => { console.log(` ${FAIL} ${domain.padEnd(30)} failed`); resolve(); });
});
}
}
async function main() {
console.log(`${BOLD}🔍 stackcheck${RESET} — ${new Date().toISOString()}\n`);
const domains = process.argv.slice(2).filter(a => !a.startsWith('-'));
checkUptime();
checkMemory();
checkDisk();
checkDocker();
checkPorts();
await checkSSL(domains);
console.log(`\n${BOLD}Done.${RESET}`);
}
main();
Running It
# Make it executable
chmod +x stackcheck.js
# Basic health check
./stackcheck.js
# Include SSL certificate checks
./stackcheck.js yourdomain.com nextcloud.yourdomain.com
That’s it. No npm install, no Docker image, no config files.
What the Output Looks Like
Warning Thresholds
The script uses simple, sensible thresholds:
- Memory/Disk: ✓ under 75% → ⚠ 75-89% → ✗ 90%+
- CPU Load: ✓ under 70% of cores → ⚠ 70-100% → ✗ over 100%
- SSL: ✓ 30+ days → ⚠ 8-30 days → ✗ 7 days or less
- Docker: ✓ running → ✗ exited/dead
Automate It with Cron
Run it every 6 hours and log the output:
# Add to crontab
crontab -e
# Every 6 hours, check and log
0 */6 * * * /usr/bin/node /opt/stackcheck.js yourdomain.com >> /var/log/stackcheck.log 2>&1
Want email alerts? Pipe it through mail:
0 */6 * * * /usr/bin/node /opt/stackcheck.js yourdomain.com | mail -s "Stack Health Report" [email protected]
When to Use This vs. Full Monitoring
Use stackcheck when:
- You manage 1-3 servers and want a quick glance
- You’re SSHing in anyway and want a health summary
- You don’t want to maintain a monitoring stack
- You need something that works on minimal installs
Use Uptime Kuma / Prometheus / Grafana when:
- You need historical data and graphs
- You need real-time alerting to Slack/Discord/Telegram
- You’re monitoring dozens of services across multiple hosts
- You need public status pages
There’s no reason you can’t use both. Run stackcheck for quick manual checks and Uptime Kuma for ongoing monitoring.
Extending It
The script is intentionally simple. Fork it and add whatever you need:
- Backup age check:
statyour backup directory, warn if older than 24h - Service-specific health: curl your Nextcloud/Jellyfin health endpoints
- Temperature: read
/sys/class/thermal/thermal_zone*/tempon bare metal - SMART disk health: parse
smartctloutput
The whole point is that it’s one file you understand completely. No framework, no abstractions, no surprises.
Wrapping Up
Monitoring doesn’t have to be complicated. Sometimes the best tool is a script you can read top to bottom in five minutes. Copy stackcheck.js to your server, run it, and know exactly where you stand.