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.
The full compose service
Section titled “The full compose service”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_healthyThe 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.
Image tags and platforms
Section titled “Image tags and platforms”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.
Environment variables
Section titled “Environment variables”| 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).
Run without Compose
Section titled “Run without Compose”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:
- A reachable Docker Engine API. The agent follows
DOCKER_HOST, so point it at the proxy sidecar. - Docker labels on the containers you want tailed or scraped. Labels are a Docker feature, not a Compose feature.
/procand/sysmounted read-only, withHOST_PROCandHOST_SYSset.- 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.
With plain docker run
Section titled “With plain docker run”Create the network first, so both containers and every scrape target share it:
docker network create luminStart the proxy sidecar:
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.1Start the agent with the name lumin-agent, because the proxy accepts requests only from that name:
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:latestOpt your own containers in with the same labels the compose examples use:
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:latestStart order does not matter
Section titled “Start order does not matter”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.
The healthcheck does not carry over
Section titled “The healthcheck does not carry over”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:
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.
What else works
Section titled “What else works”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.
Monitor attached volumes
Section titled “Monitor attached volumes”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:roMount 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.
Networking
Section titled “Networking”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.
Health and readiness
Section titled “Health and readiness”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.
Resource ceilings
Section titled “Resource ceilings”mem_limit: 256mpids_limit: 256cpus: "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.
Hardening
Section titled “Hardening”The compose snippet sets:
read_only: truewithtmpfs: [/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.
Rotating the API key
Section titled “Rotating the API key”- Settings → API keys — mint a fresh key on the same project.
- Edit
LUMIN_AGENT_API_KEYin your.envwith the new value. 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.- 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.