mirror of
https://github.com/tiennm99/composes.git
synced 2026-10-11 03:13:16 +00:00
node-exporter's diskstats collector reads udev device properties from /run/udev/data; without the mount it logs an error and drops those labels.
149 lines
7.8 KiB
Markdown
149 lines
7.8 KiB
Markdown
# alloy
|
||
|
||
[Grafana Alloy](https://grafana.com/docs/alloy/latest/) shipping host and
|
||
container telemetry to Grafana Cloud, with remote config from Grafana Fleet
|
||
Management.
|
||
|
||
One container runs both the `node_exporter` (host) and `cadvisor` (container)
|
||
collectors. A second, tiny container proxies a read-only slice of the Docker
|
||
API to it. The Alloy config is embedded inline via Compose `configs:`, so
|
||
there is no `config.alloy` on disk.
|
||
|
||
`dockerproxy` publishes
|
||
`127.0.0.1:2375` — Alloy runs with host networking, so it has no compose
|
||
network to reach the proxy over.
|
||
|
||
## What it collects
|
||
|
||
| Source | Component | Notes |
|
||
|---|---|---|
|
||
| Host metrics | `prometheus.exporter.unix` | CPU, memory, load, disk I/O, filesystem, network, uname, boot time, systemd, vmstat, sockstat — the default collector set minus `ipvs/btrfs/infiniband/xfs/zfs` |
|
||
| Container metrics | `prometheus.exporter.cadvisor` | CPU, memory, fs usage/limit, network, `last_seen`. Docker labels are not copied onto the series (`store_container_labels = false`): Coolify gives its containers 50–80 labels, which pushes series past Grafana Cloud's 60-label limit and gets them rejected |
|
||
| Container logs | `loki.source.docker` | All running containers, labeled `container`, `stream`, `instance` |
|
||
| Journal logs | `loki.source.journal` | systemd journal, labeled `unit`, `boot_id`, `transport`, `level` |
|
||
| File logs | `loki.source.file` | `/var/log/syslog`, `/var/log/messages`, `/var/log/*.log` |
|
||
| Remote config | `remotecfg` | Polls Grafana Fleet Management every 60s |
|
||
|
||
Metric filtering copies the `keep`-lists from the upstream Grafana Cloud
|
||
integrations verbatim ([Linux Node](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-linux-node/#metrics),
|
||
[Docker](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-docker/#metrics)).
|
||
Logs are unfiltered.
|
||
|
||
## Environment
|
||
|
||
All nine are required; `docker compose up` fails fast if any is unset.
|
||
|
||
| Variable | Purpose |
|
||
| --- | --- |
|
||
| `ALLOY_HOSTNAME` | Container hostname, and the Loki/Prometheus `instance` label |
|
||
| `REMOTECFG_URL` | Fleet Management endpoint |
|
||
| `REMOTECFG_ID` | Fleet Management agent id |
|
||
| `REMOTECFG_USER` | Fleet Management user id |
|
||
| `PROM_URL` / `PROM_USER` | Prometheus remote-write endpoint and user id |
|
||
| `LOKI_URL` / `LOKI_USER` | Loki push endpoint and user id |
|
||
| `GRAFANA_TOKEN` | One Cloud Access Policy token, scopes `metrics:write` + `logs:write` + `fleet-management:read` |
|
||
|
||
`GRAFANA_TOKEN` is also passed into the container as `GCLOUD_RW_API_KEY`.
|
||
Fleet Management's auto-generated `self_monitoring_*` pipelines read the token
|
||
from that fixed name; without it their remote-write gets an empty password and
|
||
Grafana Cloud answers `401 invalid token`, so the collector shows no health
|
||
data in Fleet Management.
|
||
|
||
Find the values under Grafana Cloud → your stack → **Details** on each data
|
||
source, and under Fleet Management. The same token serves `remotecfg`,
|
||
Prometheus and Loki basic-auth.
|
||
|
||
```bash
|
||
cp .env.example .env # then fill in the values
|
||
docker compose up -d
|
||
```
|
||
|
||
Run the same file on every host, changing `ALLOY_HOSTNAME` and `REMOTECFG_ID`
|
||
per host. Filter in Grafana with `instance=~"..."`.
|
||
|
||
## Privileges
|
||
|
||
The upstream Grafana Cloud docker integration runs `privileged: true` with the
|
||
raw Docker socket mounted read-write. Nothing in this config needs that, and
|
||
the combination is host-root-equivalent: `privileged` grants every capability
|
||
and unmasks `/proc` and `/sys`, and a container that can talk to the Docker
|
||
socket can start another container that mounts `/` writable. The socket's `:ro`
|
||
flag does not help — it stops the socket *file* being replaced, not the API
|
||
being used.
|
||
|
||
What it runs instead:
|
||
|
||
| Setting | Reason |
|
||
| --- | --- |
|
||
| `cap_drop: [ALL]` + `cap_add: [DAC_OVERRIDE]` | The image's entrypoint runs as uid 0 and reads host files owned by other users — the journal, paths under `/rootfs`, `/var/log`. Dropping every capability leaves it unable to open them, and unable to create its own storage directory. `DAC_OVERRIDE` restores exactly that and nothing else; `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE`, `MKNOD` and the rest stay dropped. |
|
||
| `no-new-privileges:true` | No setuid binary in the image can regain what was dropped. |
|
||
| `mem_limit: 2g`, `pids_limit: 512` | Steady state is around 900 MB; the limit stops a leak taking the host down with it. |
|
||
| `dockerproxy` instead of `/var/run/docker.sock` | See below. |
|
||
|
||
`read_only: true` is deliberately absent. Compose materialises an inline
|
||
`configs:` entry by writing it into the container, and refuses to do that on a
|
||
read-only service — `cannot create config ... : \`file\` is the sole supported
|
||
option`. Keeping the config inline is worth more than the read-only rootfs
|
||
here; adding it back means moving the config to a `config.alloy` file on disk
|
||
and switching the `configs:` entry to `file:`.
|
||
|
||
`network_mode: host` stays. `/proc/net` is a symlink to `/proc/self/net` and
|
||
resolves against the reading process's network namespace, so bind-mounting the
|
||
host's `/proc` to `/rootproc` is not enough — without host networking the
|
||
netdev, netstat, sockstat and conntrack families would describe the container's
|
||
veth pair instead of `eth0`. That is roughly 45 of the 157 kept metrics.
|
||
|
||
`/:/rootfs:ro` also stays, and is the widest remaining exposure: the container
|
||
can read every file on the host. The filesystem collector needs to `statfs()`
|
||
each mount point, and a bind mount cannot grant that without granting reads.
|
||
Dropping it would cost disk-space monitoring. Treat the container as
|
||
secret-bearing — it holds `GRAFANA_TOKEN` regardless.
|
||
|
||
## Docker API access
|
||
|
||
`prometheus.exporter.cadvisor` and `loki.source.docker` both need the Docker
|
||
API, so it cannot simply be removed. `dockerproxy` runs
|
||
[tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)
|
||
with the socket mounted read-only and publishes it on `127.0.0.1:2375`, which
|
||
Alloy reaches over host networking.
|
||
|
||
`POST` is revoked by default in that image, which is the point: container
|
||
create, `exec`, start and kill are all refused with 403, so a compromise of
|
||
Alloy can no longer become root on the host. The granted sections are the ones
|
||
the two components actually call:
|
||
|
||
| Variable | Called by |
|
||
| --- | --- |
|
||
| `CONTAINERS` | `discovery.docker` listing, `loki.source.docker` inspect and log read, cadvisor metadata |
|
||
| `NETWORKS` | `discovery.docker` — Prometheus' Docker SD resolves network names per container, and returns **zero targets** without it |
|
||
| `IMAGES`, `INFO`, `VERSION` | cadvisor |
|
||
| `EVENTS` | cadvisor container watch (granted by default) |
|
||
|
||
What this does not do: `GET /containers/{id}/json` still returns every
|
||
container's environment variables. The proxy closes the escalation path, not
|
||
the disclosure one.
|
||
|
||
## Mounts
|
||
|
||
| Mount | Why |
|
||
|---|---|
|
||
| `/proc:/rootproc:ro` | node-exporter cpu/mem/load, via `procfs_path` |
|
||
| `/sys:/sys:ro` | node-exporter and cadvisor cgroups |
|
||
| `/:/rootfs:ro` | filesystem collector, via `rootfs_path` |
|
||
| `/dev/disk/:/dev/disk:ro` | node-exporter diskstats device labels |
|
||
| `/run/udev:/run/udev:ro` | node-exporter diskstats udev device properties (`/run/udev/data`) |
|
||
| `/var/lib/docker:ro` | cadvisor container metadata |
|
||
| `/var/log:/var/log:ro` | `loki.source.journal` and `loki.source.file` |
|
||
| `/etc/machine-id:ro` | Stable host id for the journal reader |
|
||
| `alloy-data` | WAL and remotecfg cache — the only writable path |
|
||
|
||
`dockerproxy` mounts `/var/run/docker.sock:ro` and nothing else.
|
||
|
||
|
||
## Version pinning
|
||
|
||
Both images use `latest`. Neither project publishes a moving major tag:
|
||
`grafana/alloy` ships only exact `v1.x.y` tags, and `tecnativa/docker-socket-proxy`
|
||
only exact `v0.x.y` tags (its bare `0` tag is a stale leftover). `latest` is the
|
||
closest equivalent, so new releases arrive on the next redeploy.
|