diff --git a/apps/docs/content/docs/core/autoscaling.mdx b/apps/docs/content/docs/core/autoscaling.mdx new file mode 100644 index 00000000..a70ac0e9 --- /dev/null +++ b/apps/docs/content/docs/core/autoscaling.mdx @@ -0,0 +1,128 @@ +--- +title: Autoscaling +description: "Three scaling surfaces — application replicas, live manual scaling, and cluster (node) autoscaling — all driven by the Nomad Autoscaler." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Nomploy has **three** related but distinct scaling surfaces. The word +"autoscaling" shows up in more than one place, so here's the map first: + +| Surface | Where | What it scales | +|---|---|---| +| **Application autoscaling** | Application → **Advanced** → *Autoscaling* card | An app's replica count, automatically, between min/max. | +| **Live scaling** | App → **Advanced** → *Scaling* card, and Compose → **Containers** | The running job's replicas, right now (manual, or reflecting the policy). | +| **Cluster (node) autoscaling** | Nomad → **Autoscaling** tab | The number of **nodes** — provisions/destroys cloud VMs. | + +The first two scale *your workload's replicas*; the third scales *the cluster's +machines*. All of them are driven by the Nomad Autoscaler. + +## Application autoscaling + +Turns an app's fixed replica count into a policy the Nomad Autoscaler manages. + +**Application service → Advanced tab → Autoscaling card.** Flip the switch on and +set: + +- **Min replicas** (default 1) +- **Max replicas** (default 3) +- **CPU target %** (optional) — e.g. `70` +- **Memory target %** (optional) — e.g. `80` + +Set at least one of CPU / memory target (you'll get a validation error +otherwise), and keep max ≥ min. Click **Save**, then **redeploy** the app to +apply — the policy is baked into the job's `scaling` block on the next deploy. +With autoscaling **off**, the fixed replica count is used. + +Behind the scenes this emits a Nomad `scaling { min, max, enabled = true, policy +{ … } }` block with `cpu`/`memory` checks against the `nomad-apm` source, using a +`target-value` strategy. + +## Live scaling + +The **Scaling** card (app → Advanced; compose → Containers, Nomad only) shows +each task group with its current `{running}/{desired}` count and, if it has an +autoscaling policy, its `min–max` range and an **autoscaling** / **manual** +badge. Type a number and click **Scale** to set the count immediately +(`nomad job scale`). Useful for a quick manual bump or to observe what the +autoscaler is doing. + +## Compose service autoscaling (`x-nomad-scaling`) + +For compose services, add an `x-nomad-scaling` policy to a service to get the +same `scaling` block in the generated job — min/max plus CPU/memory targets — +without the app-level form. + +## Cluster (node) autoscaling + +The Nomad page's **Autoscaling** tab configures the *node* autoscaler: the cloud +provider **token** and the **SSH key** used to provision and join new machines. +This is also the prerequisite for the Cluster tab's one-click **Add node**. It +scales the fleet of servers/workers, not your app replicas. + +### Config knobs + +Per autoscaling group (Nomad node pool): + +| Knob | Meaning | +|---|---| +| **enabled** | Master switch for the group's reconcile loop. | +| **min / max** | Hard floor / ceiling on the **total** worker count in the pool (manual + autoscaled). | +| **desired** | The current target the loop drives toward, clamped to `[min, max]`. Manual sets, scheduled actions, and reactive pressure all move this. | +| **scale-up CPU % / mem %** | Reserved CPU / memory at or above which the loop nudges the target up (defaults 80 / 75). | +| **scale-down CPU % / mem %** | Reserved CPU / memory at or below which — both, with no blocked evals — it nudges the target down (default 25 / 25). | +| **cooldown** | Minimum seconds between scale actions, so one node moves per cooldown instead of thrashing (default 300). | +| **server type / location** | The cloud VM spec + region provisioned for new autoscaled nodes. | + +### How it decides (desired-count model) + +Each tick (~60s) the loop, per group: + +1. Measures **reserved** CPU/memory — what Nomad has *scheduled* onto the pool's + ready+eligible nodes (not live usage) — and counts **blocked evaluations** + targeting the pool (allocations that couldn't be placed). +2. **Converges to `desired` first.** If `workerCount < desired` it scales **up**; + if `workerCount > desired` it scales **down** — but only ever removes an + **autoscaled** node. If the surplus is all manual/pinned it reports + `none (above desired … but the extras are pinned/manual)`. +3. **Only when `workerCount == desired`** does it evaluate pressure: reserved + CPU/mem over the scale-up threshold (or any blocked eval) nudges `desired +1`; + slack under *both* scale-down thresholds nudges `desired −1` (never below + `min`). Because the nudge only happens *at* target, `desired` never runs more + than one node ahead of reality, and the cooldown paces one change per interval. + +Scale-up provisions a cloud VM (hostname-safe name), joins it to the pool, and it +becomes a ready worker. Scale-down drains an autoscaled worker, removes it, and +**destroys its VM** — so idle capacity stops billing. + +### Manual (pinned) nodes and the `min` floor + +A node added manually / one-click (`autoscaled = false`) is **pinned**: it counts +toward `workerCount` but is **never reclaimed** by scale-down. That interacts with +`min` in a way that's easy to get wrong: + + +If a pool has one pinned worker but `min = 0 / desired = 0`, the loop is +permanently in the "`workerCount(1) > desired(0)` but pinned" branch and **never +reaches the pressure check** — so it will *never* scale up under load. Set +`min = 1 / desired = 1` (with the pinned worker as the floor) and the loop sits +"at desired," free to nudge up under load and back down when idle, with **zero +extra idle cost** beyond the node you already run. + + +### Example: elastic with a pinned baseline + +One pinned manual worker, burst up to two more under load: + +``` +min = 1, desired = 1, max = 3 +scale-up CPU ≥ 80% or mem ≥ 75% (or any blocked eval) +scale-down CPU ≤ 25% and mem ≤ 25% +cooldown 300s +``` + +- **Idle:** stays at the 1 pinned worker; no autoscaled VMs, no extra cost. +- **Load:** reserved CPU crosses 80% → provisions an autoscaled VM (one per + cooldown) up to `max = 3`. +- **Load clears:** reserved CPU/mem fall under 25% → removes autoscaled VMs and + destroys them, back down to the pinned floor of 1. diff --git a/apps/docs/content/docs/core/load-balancer.mdx b/apps/docs/content/docs/core/load-balancer.mdx new file mode 100644 index 00000000..eb0a626e --- /dev/null +++ b/apps/docs/content/docs/core/load-balancer.mdx @@ -0,0 +1,99 @@ +--- +title: Load Balancer (HA ingress) +description: "Turn the single hub Traefik into a highly-available, active/active Traefik pool with DNS failover, shared certificates, metrics and logs." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Nomploy's default ingress is a single Traefik on the hub. The **Load Balancer** +tab turns that into a **highly-available, active/active Traefik pool** running on +every node you tag, with DNS-based failover and its own metrics and logs. + +Open it from the sidebar (**Load Balancer**). It has three tabs: **Overview**, +**Metrics**, and **Logs**. + +## How it works + +- **Pool** — a Nomad `system` job (`nomploy-traefik-ha`) runs one Traefik per node + whose Nomad meta has `nomploy_lb = "true"`. The hub is deliberately **excluded** + (it keeps the standalone Traefik), so standing the pool up never clashes on + `:80/:443` there and causes no downtime. +- **Routing** — each instance reads routes from the local Consul catalog, exactly + like the hub, so every app/domain is served by every instance. +- **Certificates** — TLS certs are shared through **Consul KV** (Traefik's `consul` + KV provider). Deploying the pool seeds the current certs from the hub's + `acme.json`; a background loop **re-syncs every 6h** so renewals propagate. The + Pool card lists each served domain with a **days-left** badge. +- **Entry / failover (DNS)** — the pool gets a **generated hostname** + (`lb-.`) whose **A records are kept equal to the healthy nodes' + public IPs** (health-prune). Point your app domains at the pool by adding a + **CNAME** to that hostname. Failover is DNS-speed (records use a 60s TTL). + +## 1. Tag the pool nodes + +Give each node you want in the pool the `nomploy_lb` meta — from the Nomad tab's +node tagging UI, or the CLI: + +```bash +nomad node meta apply -node-id= nomploy_lb=true +``` + +Leave the hub untagged. + +## 2. Deploy the pool + +**Load Balancer → Overview → Deploy.** This seeds the certs into Consul KV and +runs the system job on every tagged node. The member list shows each instance, +its public IP, and status. Use **Sync certs** to force a cert re-seed, or **Stop** +to remove the pool (the hub keeps serving). + +## 3. Configure DNS (optional but recommended) + +Requires a DNS provider in **Settings → DNS Providers** (Cloudflare). + +On the **Overview → DNS** card: + +1. Pick the DNS provider and zone, then **Generate hostname**. +2. Flip **Manage DNS** on. The controller creates A records for the hostname + pointing at the healthy nodes and keeps them pruned (a 30s loop, plus + **Reconcile now**). +3. **CNAME** your app domains to the generated hostname — the pool already serves + their routes and certs. + + +If your zone has a proxied wildcard (`*.example.com`), the explicit A records +nomploy creates are more specific and take precedence, so the hostname resolves +straight to your nodes. + + +## Metrics + +The **Metrics** tab shows pool-wide **throughput** (requests/s split by +2xx/4xx/5xx) and **average latency**, over a selectable range (**1h / 6h / 24h / +7d**), plus a live per-instance table (rate, status classes, latency, health, +in-DNS). Data comes from Traefik's Prometheus endpoint, sampled every 60s into a +rolling 7-day series. + +## Logs + +The **Logs** tab shows **consolidated** Traefik logs across the whole pool, each +line **tagged with the instance** it came from. The access log is parsed into a +searchable, color-coded table (time, instance, status, method, host/path, +duration, client); switch to **Errors** for the app log. Filter with the search +box, **4xx/5xx only**, or **Pause** the 5s auto-refresh. + +## Notes & limits + +- After enabling metrics/logs for the first time, **Redeploy** the pool so it + exposes the Prometheus `:8082` entrypoint and the access-log config. +- Public IPs are auto-detected from the Hetzner API (via your **Settings → Cloud** + token); a node with no detected public IP is flagged and won't be published. +- The pool serves TLS but does not issue it — a dormant `letsencrypt` resolver is + defined only so catalog routers validate; issuance/renewal stays on the hub. + + +With the HA pool in front of **all** domains, Let's Encrypt **HTTP-01** no longer +works (any node may answer the challenge). Switch issuance to **DNS-01** by +configuring a DNS provider and redeploying your apps. See **Settings → DNS +Providers**. + diff --git a/apps/docs/content/docs/core/meta.json b/apps/docs/content/docs/core/meta.json index 893ca636..2552bdd6 100644 --- a/apps/docs/content/docs/core/meta.json +++ b/apps/docs/content/docs/core/meta.json @@ -29,6 +29,7 @@ "account-security", "(Notifications)", "registry", + "self-hosted-registry", "secrets-providers", "dns-providers", "ssh-keys", @@ -36,11 +37,13 @@ "backups", "concurrent-builds", "---Services---", + "overview", "variables", "domains", "applications", "docker-compose", "databases", + "nomad-packs", "(examples)", "auto-deploy", "schedule-jobs", @@ -58,6 +61,10 @@ "remote-servers/validate", "---Advanced---", "cluster", + "load-balancer", + "autoscaling", + "telemetry", + "security", "---Enterprise---", "enterprise/index", "enterprise/license-keys", diff --git a/apps/docs/content/docs/core/nomad-packs.mdx b/apps/docs/content/docs/core/nomad-packs.mdx new file mode 100644 index 00000000..1bc29a48 --- /dev/null +++ b/apps/docs/content/docs/core/nomad-packs.mdx @@ -0,0 +1,46 @@ +--- +title: Nomad Pack deploy +description: "Deploy Nomad Packs from a one-click store gallery or any git registry — a variables form, deep-links, and a post-deploy smoke test, no HCL by hand." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +[Nomad Packs](https://developer.hashicorp.com/nomad/tools/nomad-pack) are +parameterised, reusable job templates. Nomploy can deploy them as a service type — +either hand-picked from a git registry, or **one-click from a built-in store +gallery** — so you get databases, tooling and infra without writing HCL. + +## Store gallery (one-click) + +The pack **store** lists curated packs from +[`Nomploy/nomad-packs`](https://github.com/Nomploy/nomad-packs) as cards with +**category, icon, stars and a health signal**. Pick one, fill its variables, and +deploy — the store deep-links straight into a pre-configured Create Service flow. + +## Deploy a pack manually + +Create a **Compose** service and choose type **Nomad Pack**. The Deploy Settings +card (**Nomad Pack**) has: + +- **Pack** — the pack name, e.g. `traefik`, `zot`, or `hello_world`. +- **Custom registry (optional)** — a git URL. Leave blank for the + [community registry](https://github.com/hashicorp/nomad-pack-community-registry); + set it to `github.com/Nomploy/nomad-packs` (or your own) and it's added before + the run. +- **Variables (HCL)** — pack variables (`count = 2`, `region = "global"`, …), + passed as `--var-file`. Leave empty to use the pack's defaults. + +Click **Save**, then **Deploy** to run the pack (`nomad-pack run`). Stop / delete +tear it down (`nomad-pack destroy`). After a deploy, a **smoke test** reports +whether the pack's allocations came up healthy. + + +Pack and registry values are validated to a safe character set, since they become +part of the `nomad-pack` command line. + + +## Using your own registry + +Point **Custom registry** at any git repo that holds packs. Nomploy adds it +(`nomad-pack registry add`) before running, so private/internal pack libraries +work the same way as the community one. diff --git a/apps/docs/content/docs/core/overview.mdx b/apps/docs/content/docs/core/overview.mdx new file mode 100644 index 00000000..b752a725 --- /dev/null +++ b/apps/docs/content/docs/core/overview.mdx @@ -0,0 +1,22 @@ +--- +title: Overview +description: "A single cross-project dashboard that aggregates every service, backup, domain and deployment across your whole organization." +--- + +The **Overview** page (sidebar → **Overview**) is a single, organization-wide +dashboard. Instead of opening each project to see what's running, it aggregates +the whole org into one view. + +## What it shows + +- **Services** — every application and compose service across all projects, with + its status, so you can spot anything unhealthy at a glance. +- **Domains** — all configured domains in one list, with their target service and + certificate state. +- **Backups** — scheduled database/volume backups across every project and when + each last ran. +- **Deployments** — recent deployment activity org-wide. + +Each row links straight through to the underlying service, domain, backup or +deployment, so Overview works as a jumping-off point as well as a status board — +useful once you run more than a couple of projects on the cluster. diff --git a/apps/docs/content/docs/core/security.mdx b/apps/docs/content/docs/core/security.mdx new file mode 100644 index 00000000..872eb421 --- /dev/null +++ b/apps/docs/content/docs/core/security.mdx @@ -0,0 +1,104 @@ +--- +title: Security (CrowdSec) +description: "Built-in CrowdSec detects malicious HTTP traffic and blocks offending IPs at the ingress across every load-balancer node — fully local and private." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Nomploy ships a built-in **[CrowdSec](https://www.crowdsec.net/)** integration +that detects malicious HTTP traffic (scanning, probing, backdoor/CVE attempts, +aggressive crawling) from your Traefik access logs and can **block the offending +IPs at the ingress** — across every load-balancer node. + +Open it from the sidebar (**Security**). It runs fully **locally and privately**: +no per-request calls leave the cluster, and the CrowdSec Central API is disabled. + +## How it works + +Two halves — a **brain** (detection) and **bouncers** (enforcement): + +- **Brain — CrowdSec engine (LAPI + agent).** A single Nomad job + (`nomploy-crowdsec`) runs on the hub. Its agent tails the hub Traefik access + log and runs it through the `crowdsecurity/traefik` parsers + scenarios. When an + IP trips a scenario (e.g. too many 4xx, known backdoor paths), the engine stores + a **decision** (a ban: this IP/range, for a duration) in the **Local API + (LAPI)**. Detection alone blocks nothing — it just maintains the ban list. +- **Bouncers.** The CrowdSec **Traefik plugin** runs as a middleware on every + ingress Traefik — the HA pool nodes **and** the hub's standalone Traefik (so all + nodes in the LB DNS are covered). In **stream mode** each bouncer pulls the ban + list from LAPI every ~60s and caches it; on each request it checks the client IP + against the cache — banned → blocked, otherwise passes. + +``` +request → Traefik (access log) → agent parses → scenario → ban in LAPI → +bouncers pull it (~60s) → that IP is blocked on every node +``` + +Evaluation is entirely local: rules (collections) are downloaded once at startup, +then matching runs in-process. The community Central API is off, so **nothing is +shared** and no community blocklist is pulled — bans come only from your own +traffic. + +## Detection vs enforcement + +Two toggles on the Security page: + +- **Enable** — run the detector. You see the decisions (would-be bans) it + produces, but nothing is blocked. Safe to run on its own to observe. +- **Enforce** — turn on the bouncers so banned IPs are actually blocked at the + load balancer. + +## Anti-lockout + +The bouncer is given a **trusted-IP allowlist that always bypasses it**: + +- the **WireGuard overlay subnet** (`10.10.0.0/24`) is always included, so the + cluster, panel and intra-node traffic can never be blocked; +- plus any **CIDRs/IPs you add** on the Security page. + + +The hub bouncer also covers the panel's own route, so an allowlisted admin IP +guarantees you keep access. Recovery from a bad enforcement change is to toggle +**Enforce** off — the panel runs on the hub Traefik, independent of the pool. + + +## Enable it + +1. Sidebar → **Security**. +2. **Enable CrowdSec** → Save. The detector deploys on the hub and starts listing + decisions (it auto-enables the hub's file access log it needs). +3. Add your **admin IP** to the allowlist. +4. Turn on **Enforce** → Save to start blocking. Use the decisions table to + **unban** an IP/range if needed. The **Active bans** chart shows the ban count + over time. + +## Security & observability alerts + +The **Alerts** page (sidebar → **Alerts**) can watch cluster-wide health metrics +and notify your cluster-alert channels on a threshold breach. Alongside the load +balancer and per-service metrics, a starter set covers security and observability: + +| Metric | Fires when | Typical use | +|---|---|---| +| `crowdsec_active_bans` | active bans **>** a threshold | spot an attack wave / runaway bans | +| `crowdsec_detector_up` | detector **not** running | CrowdSec brain is down | +| `otel_collector_up` | collector **not** running | telemetry pipeline is down | +| `log_agent_up` | some node's log agent is down | log shipping gap | + +Each metric is cluster-wide (no target) and reports "not applicable" when its +feature is turned off, so *off* never reads as *down*. The health metrics are +**debounced** — a breach must hold for the rule's window before it fires, so a +brief dip during a rolling update doesn't flap the alert. + +## Notes + +- **Scaling.** The pool bouncer is a Nomad system job, so any new node you tag + `nomploy_lb` runs it automatically; the WG subnet in the allowlist covers new + nodes' IPs, and all bouncers pull from the one LAPI on the hub. Adding nodes + needs nothing extra. +- **Plugin fetch.** The Traefik bouncer plugin is fetched from + `plugins.traefik.io` at Traefik startup. For full offline resilience, vendor it + into a custom Traefik image. +- **Logs source.** Detection currently parses the **hub** Traefik access log; the + HA pool ships its access logs to your telemetry backend via OTLP (see + [Telemetry](/docs/core/telemetry)). diff --git a/apps/docs/content/docs/core/self-hosted-registry.mdx b/apps/docs/content/docs/core/self-hosted-registry.mdx new file mode 100644 index 00000000..ae06f9eb --- /dev/null +++ b/apps/docs/content/docs/core/self-hosted-registry.mdx @@ -0,0 +1,74 @@ +--- +title: Self-hosted registry +description: "Provision an S3/R2-backed zot registry in one click so build-from-source apps run across a multi-node cluster — or bring your own and register it." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +On a multi-node cluster, a build-from-source app is built on **one** node but may +be scheduled on **any** node, so the built image has to be pullable from somewhere +every node can reach. Nomploy gives you two ways to get there: a **one-click +self-hosted registry**, or **bring your own** and register it. + +## Do you even need one? + +Only for **build-from-source on a multi-node cluster**. You can skip a registry +entirely when: + +- **Single-node** — build and run on the same box (the local image store is enough). +- **You only run pre-built images** (templates like `postgres`, `redis`, or + `image: ghcr.io/you/app`) — every node pulls from the public source directly. + +## One-click self-hosted (zot + S3/R2) + +**Settings → Registry → Self-hosted (zot + S3)** provisions an internal +[zot](https://zotregistry.dev/) OCI registry whose storage is backed by an +**S3-compatible bucket** (AWS S3, Cloudflare R2, …). Because the blobs live in +object storage rather than on one node's disk, the registry is stateless and +every node can pull from it. + +It's fronted by Traefik with a **real TLS certificate** issued via **DNS-01**, so +there's no per-node `insecure-registries` daemon tweak to make — the cluster +trusts it out of the box. Build-from-source apps then push here on deploy and run +cluster-wide. + + +You provide an S3/R2 bucket + credentials and a hostname (with a DNS provider +configured for DNS-01). Nomploy provisions zot, wires the cert, and registers the +registry so builds use it automatically. + + +## Bring your own + +Any OCI registry works — your own zot, a plain `registry:2`, GHCR, ECR, Docker +Hub. Run it however you like (a normal Docker service, or the `zot` pack from +[`Nomploy/nomad-packs`](https://github.com/Nomploy/nomad-packs)) and register it: + +**Settings → Registry → Add Registry:** + +- **Registry URL** — e.g. `10.10.0.1:5000` or your registry's host +- **Username / Password** — push credentials (self-hosted registries can be + anonymous-pull, so only push needs them) +- **Image prefix** — e.g. `apps` + +Nomploy then pushes to `//` from the control plane, and +every node pulls it. + +## How the cluster trusts + uses it + +Two things every node needs: + +1. **Trust (TLS).** A registry with a real HTTPS cert (like the one-click option) + needs nothing. A plain **HTTP** registry needs its address in each node's + Docker `insecure-registries` (`/etc/docker/daemon.json` + `systemctl reload + docker`) — a per-node daemon setting with no per-job workaround. +2. **Auth (credentials).** Handled for you: **Add Registry** publishes the merged + docker auth to **Consul KV**, and consul-template on every node renders it to + `/root/.docker/config.json` (refreshed when you change a registry). Private + images pull cluster-wide **without any credentials in job specs** — the Nomad + equivalent of Swarm's `--with-registry-auth`. + + +Consul KV is plaintext, gated only by Consul ACLs — fine for a single-tenant +private (WireGuard) cluster; use Vault for a shared/multi-tenant one. + diff --git a/apps/docs/content/docs/core/telemetry.mdx b/apps/docs/content/docs/core/telemetry.mdx new file mode 100644 index 00000000..7f8b4fa0 --- /dev/null +++ b/apps/docs/content/docs/core/telemetry.mdx @@ -0,0 +1,68 @@ +--- +title: Telemetry (Observability) +description: "A built-in OpenTelemetry Collector ships cluster metrics and logs to any OTLP backend (SigNoz, Grafana, Datadog, …) with service auto-discovery." +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +Nomploy runs a built-in **OpenTelemetry Collector** that gathers metrics and logs +from across the cluster and forwards them, in the open **OTLP** format, to any +backend you point it at — [SigNoz](https://signoz.io/), Grafana/Tempo/Loki, +Datadog, Honeycomb, or your own collector. + +Open it from the sidebar (**Observability**). Nothing is sent anywhere until you +enable it and set a destination. + +## How it works + +- **Collector** — a Nomad job runs the OpenTelemetry Collector on the cluster. It + **auto-discovers** scrape targets from the **Consul catalog** (every service + nomploy registers), so you don't maintain a scrape list by hand. +- **Export** — everything is forwarded over **OTLP** (gRPC/HTTP) to the endpoint + you configure, with an optional **auth profile** (named header/token sets) so + you can target managed backends that require an ingestion key. +- **Per-node log agent** — when log shipping is on, a Nomad **system** job runs one + lightweight agent per node that tails each local allocation's stdout/stderr and + ships it as OTLP logs (one agent per node, so it can read that node's own logs). + +## Signals (toggles) + +Turn on only what you want to ship: + +- **Service metrics** — per-service CPU/memory/throughput, auto-discovered from + Consul. +- **Nomad metrics** — the Nomad runtime's own telemetry (schedulers, clients, + allocations). +- **Service logs** — application stdout/stderr from every node (via the per-node + log agent). +- **Load-balancer logs** — the HA Traefik pool's access logs, shipped natively + over OTLP straight from Traefik. + +## Set it up + +1. Sidebar → **Observability**. +2. Set the **OTLP endpoint** of your backend and, if needed, an **auth profile** + (e.g. an ingestion key header). +3. Enable the **signals** you want (metrics, Nomad metrics, service logs, LB logs). +4. **Save** — the collector (and, for service logs, the per-node agent) deploys + automatically and starts forwarding. It self-heals on a panel roll or node loss. + + +If you use SigNoz, nomploy ships importable dashboards for the cluster, services +and load balancer. Grab the JSON from the project's +`docs/observability/signoz/` directory and import them in SigNoz. + + +## Alerts + +Observability health is alertable from the **Alerts** page: `otel_collector_up` +fires if the collector stops running, and `log_agent_up` fires if any node's log +agent is down — so a telemetry gap pages you instead of going unnoticed. See +[Security & observability alerts](/docs/core/security#security--observability-alerts). + +## Notes + +- The collector and agents are right-sized automatically; memory per task scales + with the number of discovered targets. +- Because export is plain OTLP, you are never locked to one backend — point it at + a different endpoint and redeploy. diff --git a/apps/website/components/first-features.tsx b/apps/website/components/first-features.tsx index 78cf5658..7dbd2390 100644 --- a/apps/website/components/first-features.tsx +++ b/apps/website/components/first-features.tsx @@ -11,6 +11,7 @@ import { IconUsers, } from "@tabler/icons-react"; import { + Activity, Boxes, Cpu, Layers, @@ -18,6 +19,7 @@ import { Network, Repeat, Scaling, + ShieldCheck, UnlockIcon, } from "lucide-react"; @@ -83,6 +85,18 @@ export function FirstFeaturesSection() { "Real-time CPU, memory, disk and network graphs across every node, with alerts when your thresholds are crossed.", icon: , }, + { + title: "Built-in Security (CrowdSec)", + description: + "Detect malicious traffic from your access logs and block offending IPs at every ingress node — fully local, with an anti-lockout allowlist.", + icon: , + }, + { + title: "OpenTelemetry Pipeline", + description: + "Ship cluster metrics and logs to any OTLP backend (SigNoz, Grafana, Datadog) with service auto-discovery from the Consul catalog.", + icon: , + }, { title: "GPU Scheduling", description: