tutorial

How to Monitor Nixpacks with Vigilmon

Nixpacks automates build and deployment pipelines for modern apps — but silent build failures, dependency resolution hangs, and cache invalidation storms can stall your deploys invisibly. Learn how to monitor build success rates, dependency resolution health, build cache efficiency, and deploy pipeline status with Vigilmon.

Nixpacks is an open-source build system that automatically detects your application's language and framework, resolves dependencies through Nix, and produces OCI-compatible container images — without a Dockerfile. It powers the build pipeline behind Railway's platform and is widely used for zero-config deployments of Node.js, Python, Ruby, Go, Rust, PHP, and dozens of other runtimes. Nixpacks reads your project, pins dependencies via Nix packages, and generates reproducible builds with a deterministic layer cache.

But Nixpacks build failures are quiet in the ways that matter most. A dependency that failed to resolve and fell back to a cached version that doesn't match your lockfile, a Nix package download that timed out and retried silently, a build cache that was invalidated and is now rebuilding from scratch on every commit, or a deploy that completed without error but deployed an older image — these conditions produce no alert by default. Vigilmon gives you the external visibility layer your build and deploy pipeline needs: build outcome monitoring, cache hit rate tracking, deploy verification checks, and heartbeat monitoring for the deployed applications themselves.


Why Nixpacks Needs External Monitoring

Nixpacks runs inside your CI/CD pipeline and produces images — monitoring must span both the build system and the deployed artifacts:

  • Build success rate monitoring catches rising failure rates before they block your entire deploy pipeline
  • Dependency resolution health checks detect Nix store download failures and lockfile drift early
  • Build cache hit rate tracking alerts when cache invalidation is causing expensive full rebuilds
  • Deploy pipeline status verifies that a build reaching "success" actually produced a running application
  • Application heartbeat monitoring proves that Nixpacks-deployed applications are alive and serving traffic

Step 1: Instrument Your Nixpacks Build Pipeline

Nixpacks is invoked as a CLI (nixpacks build) inside your CI pipeline. Start by capturing build outcomes and timing:

# build_with_metrics.sh
#!/bin/bash
set -euo pipefail

APP_NAME="${1:?Usage: $0 <app-name> [context-dir]}"
CONTEXT_DIR="${2:-.}"
METRICS_FILE="/tmp/nixpacks_metrics_${APP_NAME}.json"
BUILD_LOG="/tmp/nixpacks_build_${APP_NAME}.log"

START_TIME=$(date +%s%3N)

# Run build, capture exit code without failing the script immediately
nixpacks build "$CONTEXT_DIR" \
    --name "$APP_NAME" \
    --out ".nixpacks" \
    2>&1 | tee "$BUILD_LOG"
BUILD_EXIT=$?

END_TIME=$(date +%s%3N)
DURATION_MS=$((END_TIME - START_TIME))

# Detect cache hits from build log
CACHE_HITS=$(grep -c "cached" "$BUILD_LOG" 2>/dev/null || echo 0)
CACHE_MISSES=$(grep -c "downloading" "$BUILD_LOG" 2>/dev/null || echo 0)

# Write metrics JSON
cat > "$METRICS_FILE" <<EOF
{
  "app": "$APP_NAME",
  "success": $([ $BUILD_EXIT -eq 0 ] && echo true || echo false),
  "exit_code": $BUILD_EXIT,
  "duration_ms": $DURATION_MS,
  "cache_hits": $CACHE_HITS,
  "cache_misses": $CACHE_MISSES,
  "timestamp": $START_TIME
}
EOF

echo "Build complete: exit=$BUILD_EXIT duration=${DURATION_MS}ms"
exit $BUILD_EXIT

Store these metrics files where your health endpoint can read them, or push them to a metrics aggregator.


Step 2: Build a Nixpacks Pipeline Health Endpoint

Create a health endpoint that aggregates recent build outcomes and exposes them for Vigilmon:

# nixpacks_health.py
import json, os, glob, time
from flask import Flask, jsonify
from pathlib import Path

app = Flask(__name__)

METRICS_DIR       = os.environ.get('NIXPACKS_METRICS_DIR', '/tmp')
METRICS_PREFIX    = os.environ.get('NIXPACKS_METRICS_PREFIX', 'nixpacks_metrics_')
BUILD_FAIL_THRESH = int(os.environ.get('BUILD_FAIL_THRESHOLD', '3'))
STALE_BUILD_SECS  = int(os.environ.get('STALE_BUILD_SECS', '7200'))  # 2 hours
CACHE_HIT_MIN_PCT = float(os.environ.get('CACHE_HIT_MIN_PCT', '0.3'))
BUILD_DUR_WARN_MS = int(os.environ.get('BUILD_DUR_WARN_MS', '300000'))  # 5 min

def load_recent_metrics():
    pattern = str(Path(METRICS_DIR) / f"{METRICS_PREFIX}*.json")
    files = glob.glob(pattern)
    metrics = []
    for f in files:
        try:
            data = json.loads(Path(f).read_text())
            metrics.append(data)
        except Exception:
            pass
    return sorted(metrics, key=lambda m: m.get('timestamp', 0), reverse=True)

@app.route('/health/nixpacks')
def health():
    issues = []
    all_metrics = load_recent_metrics()

    if not all_metrics:
        return jsonify(
            status='unknown',
            reason='no_build_metrics_found',
            note='Run at least one build with metrics instrumentation first',
        ), 200

    now_ms = int(time.time() * 1000)
    recent = [m for m in all_metrics if (now_ms - m.get('timestamp', 0)) < STALE_BUILD_SECS * 1000]

    if not recent:
        issues.append(f'no_builds_in_last_{STALE_BUILD_SECS}s')
    else:
        failures = [m for m in recent if not m.get('success', False)]
        if len(failures) >= BUILD_FAIL_THRESH:
            issues.append(f'consecutive_build_failures_{len(failures)}')

        total_cache = sum(m.get('cache_hits', 0) + m.get('cache_misses', 0) for m in recent)
        total_hits  = sum(m.get('cache_hits', 0) for m in recent)
        cache_hit_rate = total_hits / total_cache if total_cache > 0 else 1.0
        if cache_hit_rate < CACHE_HIT_MIN_PCT and total_cache > 10:
            issues.append(f'low_cache_hit_rate_{int(cache_hit_rate * 100)}pct')

        slow_builds = [m for m in recent
                       if m.get('duration_ms', 0) > BUILD_DUR_WARN_MS]
        if slow_builds:
            issues.append(f'{len(slow_builds)}_slow_builds_over_{BUILD_DUR_WARN_MS}ms')

    latest = all_metrics[0] if all_metrics else {}

    if issues:
        return jsonify(
            status='degraded',
            issues=issues,
            latest_build=latest,
            recent_count=len(recent),
        ), 503

    return jsonify(
        status='ok',
        latest_build=latest,
        recent_count=len(recent),
        cache_hit_rate=round(cache_hit_rate * 100, 1) if recent else None,
    ), 200

@app.route('/health/nixpacks/builds')
def builds():
    metrics = load_recent_metrics()[:20]  # last 20 builds
    return jsonify(status='ok', builds=metrics), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3040)

Run the health sidecar on the same host as your CI runner or build orchestrator:

NIXPACKS_METRICS_DIR=/tmp \
BUILD_FAIL_THRESHOLD=3 \
CACHE_HIT_MIN_PCT=0.4 \
BUILD_DUR_WARN_MS=600000 \
python nixpacks_health.py

Step 3: Dependency Resolution Health Monitoring

Nixpacks resolves runtime dependencies through Nix. Nix package downloads can fail due to binary cache outages or network issues. Add a dedicated dependency resolution health check:

# dep_resolution_check.sh
#!/bin/bash
# Run before builds to verify Nix binary cache is reachable

CACHE_URL="${NIX_BINARY_CACHE:-https://cache.nixos.org}"
CHECK_PATH="${1:-nixpkgs.nodejs}"

# Check binary cache reachability
if ! curl -sf --max-time 10 "${CACHE_URL}/nix-cache-info" > /dev/null; then
    echo "WARN: Nix binary cache unreachable: ${CACHE_URL}"
    exit 1
fi

# Try resolving a known package path to validate Nix daemon health
if command -v nix-instantiate &> /dev/null; then
    timeout 30 nix-instantiate --eval -E "builtins.length (builtins.attrNames (import <nixpkgs> {}))" \
        > /dev/null 2>&1 && echo "ok: nix expression evaluation healthy" \
        || echo "WARN: nix expression evaluation failed"
fi

echo "ok: dependency resolution check passed"

Wrap this check in your health endpoint:

import subprocess

@app.route('/health/nixpacks/deps')
def deps():
    issues = []

    # Check Nix binary cache reachability
    nix_cache = os.environ.get('NIX_BINARY_CACHE', 'https://cache.nixos.org')
    try:
        r = requests.get(f'{nix_cache}/nix-cache-info', timeout=10)
        if r.status_code != 200:
            issues.append(f'nix_cache_http_{r.status_code}')
    except requests.Timeout:
        issues.append('nix_cache_timeout')
    except Exception as e:
        issues.append(f'nix_cache_error: {e}')

    # Check Nix daemon (if running locally)
    try:
        result = subprocess.run(
            ['nix', 'store', 'ping', '--json'],
            capture_output=True, text=True, timeout=10
        )
        if result.returncode != 0:
            issues.append('nix_daemon_unhealthy')
    except FileNotFoundError:
        pass  # Nix not installed on this host
    except subprocess.TimeoutExpired:
        issues.append('nix_daemon_timeout')

    if issues:
        return jsonify(status='degraded', issues=issues), 503

    return jsonify(status='ok', nix_cache=nix_cache), 200

Step 4: Configure Vigilmon HTTP Monitors

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Nixpacks health endpoint: https://your-ci-host.example.com/health/nixpacks
  4. Set the check interval to 5 minutes (builds run less frequently than typical services)
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 5000ms
  6. Under Alert channels, assign your Slack or PagerDuty integration
  7. Save the monitor

Add monitors for each pipeline health dimension:

| Monitor URL | Purpose | Interval | |---|---|---| | /health/nixpacks | Build success rate + cache hit rate | 5 min | | /health/nixpacks/deps | Nix binary cache + daemon health | 10 min | | /health/nixpacks/builds | Recent build history (audit only) | — |

For production deploy pipelines, also add monitors for the deployed applications themselves (see Step 5) — build success without application health proves nothing.


Step 5: Heartbeat Monitoring for Deployed Applications

The most important thing a Nixpacks-powered deploy pipeline can do is produce a running application. Add Vigilmon heartbeat monitoring to every application deployed through Nixpacks — this closes the loop between build success and service availability.

Set Up the Heartbeat Monitor

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: nixpacks-app-<your-app-name>
  3. Set the expected interval: 5 minutes
  4. Set the grace period: 10 minutes
  5. Save — copy the heartbeat URL: https://vigilmon.online/heartbeat/abc123xyz

Deploy-Time Verification Script

Run this after each Nixpacks build completes to verify the deployed container is actually serving:

#!/bin/bash
# post_deploy_verify.sh

APP_URL="${1:?Usage: $0 <app-url> [heartbeat-url]}"
VIGILMON_HB="${2:-}"
MAX_WAIT=120
INTERVAL=5

echo "Waiting for $APP_URL to become healthy..."

for ((i=0; i<MAX_WAIT; i+=INTERVAL)); do
    HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
        --max-time 10 "$APP_URL/health" 2>/dev/null || echo "000")

    if [[ "$HTTP_CODE" == "200" ]]; then
        echo "Deploy verified: $APP_URL returned 200 after ${i}s"
        if [[ -n "$VIGILMON_HB" ]]; then
            curl -sf "$VIGILMON_HB" > /dev/null || true
            echo "Heartbeat sent to Vigilmon"
        fi
        exit 0
    fi

    echo "  ${i}s: HTTP $HTTP_CODE — waiting..."
    sleep "$INTERVAL"
done

echo "ERROR: $APP_URL did not become healthy within ${MAX_WAIT}s"
exit 1

Integrate this into your CI pipeline after nixpacks build and container push:

# .github/workflows/deploy.yml (GitHub Actions example)
- name: Build with Nixpacks
  run: |
    nixpacks build . --name my-app
    ./build_with_metrics.sh my-app .

- name: Push image
  run: docker push my-registry/my-app:${{ github.sha }}

- name: Deploy to production
  run: ./deploy.sh my-app ${{ github.sha }}

- name: Verify deployment
  run: |
    ./post_deploy_verify.sh \
      https://my-app.example.com \
      ${{ secrets.VIGILMON_DEPLOY_HEARTBEAT_URL }}

Application-Level Heartbeat (for Long-Running Apps)

For applications that run continuously after a Nixpacks deploy, add periodic heartbeats from inside the application itself:

// heartbeat.js (Node.js — add to your app)
const VIGILMON_URL = process.env.VIGILMON_HEARTBEAT_URL;
const HEARTBEAT_INTERVAL_MS = 4 * 60 * 1000; // 4 minutes

if (VIGILMON_URL) {
  setInterval(() => {
    fetch(VIGILMON_URL).catch(() => {});
  }, HEARTBEAT_INTERVAL_MS);
}
# heartbeat.py (Python — add to your app startup)
import requests, os, threading

VIGILMON_URL = os.environ.get('VIGILMON_HEARTBEAT_URL')

def heartbeat_loop():
    if not VIGILMON_URL:
        return
    import time
    while True:
        try:
            requests.get(VIGILMON_URL, timeout=5)
        except Exception:
            pass
        time.sleep(240)  # 4 minutes

if VIGILMON_URL:
    t = threading.Thread(target=heartbeat_loop, daemon=True)
    t.start()

Step 6: Alert Routing

| Monitor | Alert Channel | Priority | |---|---|---| | Build success rate /health/nixpacks | Slack + PagerDuty | P1 | | Nix dependency cache /health/nixpacks/deps | Slack | P2 | | Deployed app HTTP monitors | PagerDuty | P1 | | Heartbeat: post-deploy verification | Slack + PagerDuty | P1 | | Heartbeat: app liveness | Slack + PagerDuty | P1 |

Threshold guidelines:

  • 3 consecutive build failures: critical — something systematic is broken in the build environment
  • < 30% cache hit rate: warn — dependency resolution is rebuilding from scratch, builds will be slow
  • > 10 minutes build duration: warn for typical web apps (Nixpacks should build in 2–5 min with warm cache)
  • > 30 minutes build duration: critical — build is likely hung on a dependency download

For teams deploying multiple times per day, alert on the build success rate trend over a rolling window: three failures in an hour is more alarming than three failures spread over a week.


Summary

Nixpacks build and deploy failures are silent in the ways that cost the most — a failed dependency resolution that deployed an old image, a hung build that blocked your team's deploy queue for an hour, or a successful build that produced a container that never started serving. External monitoring with Vigilmon catches all of these:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /health/nixpacks | Build success rate and cache hit efficiency | | HTTP monitor on /health/nixpacks/deps | Nix binary cache and daemon health | | HTTP monitor on deployed app endpoints | Application availability post-deploy | | Heartbeat: post-deploy verification | Proof that each deploy produced a live application | | Heartbeat: app liveness | Continuous runtime health of Nixpacks-deployed services |

Get started free at vigilmon.online — your first Nixpacks pipeline monitor is running in under two minutes.

Monitor your app with Vigilmon

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

Start free →