Skip to content

Install the agent

The Quickstart shows the minimum compose snippet. This page is the complete reference: every environment variable, the Docker proxy sidecar, and the operational details that matter on a real host.

services:
lumin-docker-proxy:
image: wollomatic/socket-proxy:1.13.1
restart: unless-stopped
user: "0:0"
environment:
SP_LISTENIP: 0.0.0.0
SP_ALLOWFROM: lumin-agent
SP_ALLOW_HEAD: /_ping
SP_ALLOW_GET: '/v1\.[0-9]+/containers/(json|[0-9a-f]{64}/(json|stats|logs))'
SP_ALLOWHEALTHCHECK: "true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
healthcheck:
test: ["CMD", "/healthcheck"]
interval: 5s
timeout: 3s
retries: 5
start_period: 5s
lumin-agent:
image: luminmonitor/lumin-agent:latest
restart: unless-stopped
environment:
LUMIN_API_KEY: ${LUMIN_AGENT_API_KEY}
LUMIN_ENDPOINT: https://api.getlumin.dev
LUMIN_HOSTNAME: ${HOSTNAME:-prod-1}
LUMIN_INTERVAL: 15s
DOCKER_HOST: tcp://lumin-docker-proxy:2375
HOST_PROC: /host/proc
HOST_SYS: /host/sys
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
mem_limit: 256m
pids_limit: 256
cpus: "0.5"
healthcheck:
test: ["CMD", "/usr/local/bin/lumin-agent", "--healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
depends_on:
lumin-docker-proxy:
condition: service_healthy

The proxy forwards only the five requests that the agent makes, and it accepts them only from the container named in SP_ALLOWFROM. If you give the agent service another name, change SP_ALLOWFROM to the same name. Otherwise the proxy refuses the agent with 403 Forbidden. Keep user: "0:0": the proxy cannot open the Docker socket without it. Security model explains each setting.

The image is published to Docker Hub as luminmonitor/lumin-agent. It is public — no docker login needed.

Tag Points at
latest The newest release.
0.1.0, 0.1, 0 A pinned release. Prefer this in production, so an agent restart cannot change the version under you.
edge The latest build from main. Not a release.
sha-abc1234 One exact build.

Both linux/amd64 and linux/arm64 are published in one manifest, so an Arm host (Hetzner CAX, Oracle Ampere, Raspberry Pi) pulls the same tag as an x86 host.

Variable Required Default Notes
LUMIN_API_KEY yes — A lmn_priv_… key from Settings → API keys (kind: Server / agent — the default). The agent posts logs, metrics, and snapshots; the server rejects browser-kind (lmn_pub_*) keys on these endpoints. Scoped to one project.
LUMIN_ENDPOINT yes — https://api.getlumin.dev for SaaS. Must be https:// unless the host is loopback; the agent exits with a config error otherwise.
LUMIN_INSECURE no false Set to true to allow a cleartext http:// endpoint on a non-loopback host. The API key then crosses the network unencrypted, so only use it on a trusted network.
LUMIN_HOSTNAME no container hostname Stamped on every row. Pick something stable per VM.
LUMIN_INTERVAL no 15s Snapshot tick. Lower = more chart resolution + more ingest cost.
LUMIN_DISK_MOUNTS no / Comma-separated mount paths to report disk usage for. Each path must be visible inside the agent container — see Monitor attached volumes.
DOCKER_HOST for containers — Point at the proxy sidecar, not the host socket. Unset means the agent skips Docker entirely and ships host metrics only.
HOST_PROC yes — /host/proc. Tells gopsutil to read the host’s /proc mount.
HOST_SYS yes — /host/sys. Same idea for /sys.

HOST_ETC is intentionally not set. The agent only reads Hostname, KernelArch, KernelVersion, and BootTime from the host — none of which need /etc. Mounting all of /etc would expose host files the agent never reads (sudoers, ssh host keys, cloud-init data, network configs).

Compose is a convenience, not a requirement. The agent never reads a compose file. It needs four things, and any tool that starts a Docker container can provide them:

  1. A reachable Docker Engine API. The agent follows DOCKER_HOST, so point it at the proxy sidecar.
  2. Docker labels on the containers you want tailed or scraped. Labels are a Docker feature, not a Compose feature.
  3. /proc and /sys mounted read-only, with HOST_PROC and HOST_SYS set.
  4. A user-defined bridge network, for metric scraping only.

The fourth point is the one that bites. The agent builds each scrape URL from the target’s container name, and Docker’s embedded DNS resolves container names only on a user-defined network. The default bridge has no DNS, so scraping fails there. Log tailing and container metrics are unaffected, because both go through the Docker API rather than the network.

Create the network first, so both containers and every scrape target share it:

Terminal window
docker network create lumin

Start the proxy sidecar:

Terminal window
docker run -d --name lumin-docker-proxy --network lumin \
--restart unless-stopped --user 0:0 \
-e SP_LISTENIP=0.0.0.0 -e SP_ALLOWFROM=lumin-agent -e SP_ALLOW_HEAD=/_ping \
-e 'SP_ALLOW_GET=/v1\.[0-9]+/containers/(json|[0-9a-f]{64}/(json|stats|logs))' \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
wollomatic/socket-proxy:1.13.1

Start the agent with the name lumin-agent, because the proxy accepts requests only from that name:

Terminal window
docker run -d --name lumin-agent --network lumin \
--restart unless-stopped \
-e LUMIN_API_KEY=lmn_priv_... \
-e LUMIN_ENDPOINT=https://api.getlumin.dev \
-e LUMIN_HOSTNAME=prod-1 \
-e LUMIN_INTERVAL=15s \
-e DOCKER_HOST=tcp://lumin-docker-proxy:2375 \
-e HOST_PROC=/host/proc -e HOST_SYS=/host/sys \
-v /proc:/host/proc:ro -v /sys:/host/sys:ro \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges:true \
--memory 256m --pids-limit 256 --cpus 0.5 \
luminmonitor/lumin-agent:latest

Opt your own containers in with the same labels the compose examples use:

Terminal window
docker run -d --name api --network lumin \
--label lumin.logs=true \
--label lumin.service=api \
--label lumin.metrics=true \
--label lumin.metrics.port=8080 \
--label lumin.metrics.framework=gin \
my-api:latest

There is no depends_on to replace. The agent does not dial the Docker API at startup, and it retries the connection on every discovery pass. An agent that starts before the proxy runs in host-only mode and picks up container work within 30 seconds for logs, and within one LUMIN_INTERVAL for scrapes.

Do not pass --health-cmd. The image is distroless and has no shell, but the docker run CLI always wraps the flag in CMD-SHELL:

Terminal window
docker inspect -f '{{json .Config.Healthcheck}}' lumin-agent
{"Test":["CMD-SHELL","/usr/local/bin/lumin-agent --healthcheck"]}

That test cannot execute, so the container reports unhealthy forever. The CLI offers no exec-form flag. Any tool that sets the Docker API field HealthConfig.Test directly — Compose, Swarm, Portainer — can carry the healthcheck over in the ["CMD", "/usr/local/bin/lumin-agent", "--healthcheck"] form. On the CLI, leave it unset and rely on restart: unless-stopped.

Anything that produces a labeled Docker container on a Docker daemon:

  • docker run, a systemd unit, or an Ansible play.
  • Docker Swarm, and Nomad with the docker driver.
  • A PaaS on top of Docker: Portainer, Coolify, Dokploy.

Kubernetes is not supported. Nodes run containerd through the CRI, so there is no Docker Engine API to read, and pod labels are not Docker labels. Host metrics would still flow; container metrics, log tailing, and metric scraping would not.

Podman is untested. It exposes a Docker-compatible API socket and honours container labels, so the agent should work against DOCKER_HOST pointed at podman.sock. Nobody has verified it.

The agent reports disk usage per mount path. The default is /. To add an attached volume, list its path in LUMIN_DISK_MOUNTS and bind-mount the path into the agent container. The agent reads usage with statfs, so it sees only filesystems inside its own mount namespace.

Put the change in a docker-compose.override.yml next to your compose file:

services:
lumin-agent:
environment:
LUMIN_DISK_MOUNTS: /,/mnt/data
volumes:
- /mnt/data:/mnt/data:ro

Mount the volume at the same path inside the container. The mount label on the metric rows then matches the host path. A listed path that is not mounted is skipped, so one override can serve hosts with different volumes.

Each mount shows as its own row on the host card on the Infrastructure page. The card’s Disk percent takes the worst mount, so a volume that fills up raises the warning even when the root disk is fine.

The server also derives a host.disk.used_pct gauge per mount from each snapshot, so you can alert on disk utilization. Create a rule under Alerts → New rule on host.disk.used_pct with aggregation max, for example > 90 over a 5 minute window. That rule fires when any mount crosses the threshold. Add the filter mount=/mnt/data to watch one volume with its own threshold.

The agent makes outbound HTTPS to LUMIN_ENDPOINT and outbound HTTP to the socket proxy sidecar. No inbound ports. The sidecar hop needs a user-defined bridge network, which Compose creates for you and which you create by hand otherwise — see Run without Compose.

If you run the agent on a network with internal: true, give it a way to reach the ingest host — typically by attaching it to a second bridge network that does have egress, or by routing through a reverse proxy you already operate.

The agent ships a --healthcheck subcommand that stat()s a sentinel file (/tmp/lumin-agent.health) the main loop touches after each successful snapshot send. Healthy = a successful snapshot within the last 3 tick intervals (45s at the 15s default). The image is distroless, so the healthcheck invokes the binary directly via CMD, not a shell.

Docker does not auto-restart an unhealthy container — restart: unless-stopped only fires on process exit. The healthcheck is here so operators (and any service that depends_on: { condition: service_healthy } the agent) can observe a wedged-mid-tick process distinct from a crashed one. If you want auto-heal on unhealthy, layer an autoheal sidecar — intentionally not bundled to keep the default surface minimal.

mem_limit: 256m
pids_limit: 256
cpus: "0.5"

Steady-state agent footprint is well under these numbers. The ceilings exist so a bug in a dependency (memory leak, goroutine fan-out) can’t take the host down, not to throttle normal operation.

The compose snippet sets:

  • read_only: true with tmpfs: [/tmp] — the only file the agent writes is the health sentinel on the tmpfs.
  • cap_drop: [ALL] — the agent needs no Linux capabilities: it reads /proc//sys (regular file reads as a non-root user) and dials TCP from unprivileged ports.
  • security_opt: ["no-new-privileges:true"] — blocks setuid escalation inside the container.

See Security model for the threat model and why these are not optional.

  1. Settings → API keys — mint a fresh key on the same project.
  2. Edit LUMIN_AGENT_API_KEY in your .env with the new value.
  3. docker compose up -d lumin-agent — Compose recreates the container with the new env. The resolver cache lives on the API side and is keyed by hash, so no API restart is needed.
  4. Settings → API keys — revoke the old key. Revocation invalidates the resolver cache immediately; the old key stops working on its next request, not after a 60-second TTL window.