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
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Nixpacks health endpoint:
https://your-ci-host.example.com/health/nixpacks - Set the check interval to 5 minutes (builds run less frequently than typical services)
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty integration
- 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
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
nixpacks-app-<your-app-name> - Set the expected interval: 5 minutes
- Set the grace period: 10 minutes
- 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 minutesbuild duration: warn for typical web apps (Nixpacks should build in 2–5 min with warm cache)> 30 minutesbuild 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.