Skip to content
BoringStack
Star

Alerts

5 min read

Prometheus alert rules and Alertmanager receivers ship with the observability stack. Set ALERTMANAGER_SLACK_WEBHOOK_URL in compose/.env and alerts deliver to Slack, Discord, or any Slack-compatible endpoint. Without it, alerts surface in the Alertmanager UI at :9093 only, which is useful for confirming rules fire before committing to a pager destination.

compose/prometheus/rules.yml defines four rule groups covering common failure modes. Severity labels drive Alertmanager’s re-notify cadence: severity=page repeats hourly, severity=warn every 12 hours.

  • ApiServerErrorsHigh (page): 5xx > 1% for 5min
  • ApiServerErrorsCritical (page): 5xx > 5% for 2min
  • ApiLatencyP95High (warn): p95 > 1s for 10min
  • ApiUnreachable (page): no API traffic for 5min
  • NodeEventLoopLagging (warn): p99 lag > 200ms for 5min
  • NodeEventLoopBlocked (page): p99 lag > 1s for 2min
  • NodeEventLoopSaturated (warn): ELU avg > 90% for 10min
  • PostgresDown (page): exporter can’t reach Postgres for 2min
  • PostgresConnectionsHigh (warn): > 80% of max_connections for 5min
  • PostgresReplicationLagHigh (warn): lag > 60s for 5min
  • DiskSpaceLow (warn): < 10% free for 5min
  • DiskSpaceCritical (page): < 5% free for 2min
  • MemoryPressureHigh (warn): available < 10% for 10min
  • HostCpuSaturated (warn): load5/cores > 1.5 for 15min
  • NodeExporterDown (warn): host metrics stale for 3min
  • TraefikDown (page): Traefik metrics endpoint unreachable for 2min. Traefik is the only path into the stack in prod, so this firing means the app and the public site are down.

Thresholds are conservative defaults for a single-host BoringStack deployment. Tune per workload. A busy app may legitimately push error counts or latency above what a quiet baseline catches.

Alertmanager has no native env-var substitution in its config, so the stack ships a small entrypoint.sh that renders alertmanager.yml from ALERTMANAGER_* env vars at container boot. Empty env vars are omitted entirely, so amtool check-config stays happy.

Create a Slack app and Incoming Webhook using the Slack guide. Copy the URL, then in compose/.env:

ALERTMANAGER_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...
ALERTMANAGER_SLACK_CHANNEL=#alerts

Restart: docker compose restart alertmanager. The default message template renders status, severity, component, summary, and description.

Discord accepts Slack-format payloads at a /slack suffix on its webhook URL, so the same env var delivers there:

  1. In Discord: Server Settings → Integrations → Webhooks → New Webhook. Copy the URL.
  2. Append /slack: https://discord.com/api/webhooks/.../slack.
  3. Set ALERTMANAGER_SLACK_WEBHOOK_URL to that value in compose/.env. The ALERTMANAGER_SLACK_CHANNEL is ignored (the channel is fixed when the webhook is created).
  4. Restart Alertmanager.

For custom bridges (n8n, your own alerter, PagerDuty events-v2 translators), use the generic receiver. It POSTs Alertmanager’s native JSON, which differs from Slack’s format:

ALERTMANAGER_WEBHOOK_URL=https://your-bridge.example.com/alerts

This can be set in addition to the Slack receiver; alerts fan out to both.

Leave both env vars unset. The 16 rules still fire and land in the Alertmanager UI at http://localhost:9093. This is useful for seeing what alerts look like before deciding where to page them.

Curl a fake firing alert into Alertmanager’s API directly, no waiting for a real incident:

Terminal window
curl -XPOST http://localhost:9093/api/v2/alerts \
-H 'Content-Type: application/json' \
-d '[{
"labels": {
"alertname": "TestPing",
"severity": "warn",
"component": "manual-test"
},
"annotations": {
"summary": "Manual ping from the alerts docs",
"description": "If this lands in your Slack/Discord/webhook, the receiver is wired correctly."
}
}]'

The alert auto-resolves after resolve_timeout (5 minutes) since no follow-up ping keeps it firing.

Drop new entries into compose/prometheus/rules.yml under an existing group (or create a new group). Required fields per alert: alert, expr, for (optional debounce duration), labels.severity (page or warn), annotations.summary. The 16 bundled rules are worked examples.

Hot-reload Prometheus without a restart:

Terminal window
curl -X POST http://localhost:9090/-/reload

Every alert lands in the single default receiver by default. To route specific labels to different receivers (e.g. component=database to a DBA channel), edit compose/alertmanager/entrypoint.sh directly. It’s a small shell script with cat >> "$CFG" <<EOF blocks for each receiver. Append your own routes block and matching receivers.

If the routing grows beyond a handful of cases, create a static alertmanager.yml you maintain yourself and remove the entrypoint script.

  • Observability covers the metrics and logs stack these alerts run inside of.
  • Error tracking covers Sentry/GlitchTip for catching exceptions. Alertmanager catches “metrics-shaped” failures: rates, resource pressure, unreachability. Different surfaces, same goal.