Install on Kubernetes
This guide installs the LinkMesh agent as a Kubernetes DaemonSet — one pod per node, each enrolled with its own mTLS certificate. Use it when your collector hosts live in a cluster rather than as standalone Linux boxes.
If your hosts are plain Linux (VMs, bare metal, EC2 instances), the
Add a collector flow with the one-line
curl | sh installer is simpler.
When the K8s path makes sense
One-line curl installer | Kubernetes DaemonSet | |
|---|---|---|
| Host model | One agent per Linux host | One agent pod per K8s node |
| Collector runtime | Upstream Alloy, installed by the agent | Separate workload (Alloy, otelcol…) |
| Updates | apt upgrade | Bump image tag, re-apply manifest |
| State | /etc/linkmesh-agent/ | hostPath per node |
| Fits best when | Standalone hosts, smaller fleets | Existing K8s infra, GitOps workflow |
What you’ll do
- Mint an enrollment token in the LinkMesh UI.
- Save four small manifests to disk (Namespace, ConfigMap, Secret, DaemonSet).
- Fill in three placeholders — your HTTPS host, your gRPC host, the token.
kubectl apply -fand watch each node’s agent pod register itself.
Prerequisites
- A Kubernetes cluster (any flavour — kind, k3s, EKS, GKE, AKS, on-prem).
kubectlconfigured for the cluster.- A running LinkMesh server, reachable from your pods on both HTTPS (for the one-time bootstrap call) and gRPC over TLS (for the persistent agent → server connection). One server instance is enough; to make the server itself resilient, run it highly available on an external MongoDB database.
- Two URLs handy:
- HTTPS host — e.g.
linkmesh.example.com. The bootstrap init container POSTs tohttps://<host>/api/v1/bootstrap. - gRPC endpoint — e.g.
grpc.linkmesh.example.com:50051. The agent uses this for the persistent mTLS stream.
- HTTPS host — e.g.
1. Mint an enrollment token
Open the Collectors page in your LinkMesh UI and click + Add Collector. The wizard mints a single-use token (15-minute TTL by default) and shows you both the curl one-liner and a “for Kubernetes” snippet.
Copy the token value only — you’ll paste it into a Secret below. The DaemonSet handles the bootstrap call itself; you don’t run the install script on K8s.
2. Save the manifests
Drop these four files into a directory (e.g. ~/linkmesh-k8s/). They’re
plain YAML — no Helm, no kustomize required (though a kustomize base is
shown at the end if you want it).
00-namespace.yaml
apiVersion: v1kind: Namespacemetadata: name: linkmesh-system labels: app.kubernetes.io/name: linkmesh-system app.kubernetes.io/part-of: linkmeshlinkmesh-agent-configmap.yaml.tmpl
apiVersion: v1kind: ConfigMapmetadata: name: linkmesh-agent-config namespace: linkmesh-systemdata: server-https-url: "https://__LINKMESH_SERVER__" config.yaml: | server: url: "grpcs://__LINKMESH_GRPC__" agent: id: "" environment: "production" certificates: caCertPath: "/etc/linkmesh-agent/certs/ca.pem" clientCertPath: "/etc/linkmesh-agent/certs/client.pem" clientKeyPath: "/etc/linkmesh-agent/certs/client-key.pem" logging: level: "info"linkmesh-agent-secret.yaml.tmpl
apiVersion: v1kind: Secretmetadata: name: linkmesh-agent-bootstrap namespace: linkmesh-systemtype: OpaquestringData: enrollment-token: "__LINKMESH_TOKEN__"linkmesh-agent-daemonset.yaml
apiVersion: apps/v1kind: DaemonSetmetadata: name: linkmesh-agent namespace: linkmesh-systemspec: selector: matchLabels: app.kubernetes.io/name: linkmesh-agent updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 template: metadata: labels: app.kubernetes.io/name: linkmesh-agent spec: automountServiceAccountToken: false initContainers: - name: bootstrap image: alpine:3.20 securityContext: runAsUser: 0 runAsGroup: 0 runAsNonRoot: false env: - name: LINKMESH_SERVER valueFrom: configMapKeyRef: name: linkmesh-agent-config key: server-https-url - name: LINKMESH_TOKEN valueFrom: secretKeyRef: name: linkmesh-agent-bootstrap key: enrollment-token command: ["/bin/sh", "-c"] args: - | set -eu mkdir -p /certs && chown 65532:65532 /certs && chmod 750 /certs if [ -f /certs/client.pem ] && [ -f /certs/client-key.pem ] && [ -f /certs/ca.pem ]; then echo "Certs already present on host; skipping bootstrap." exit 0 fi echo "Enrolling against ${LINKMESH_SERVER}..." apk add --no-cache curl jq >/dev/null RESP=$(curl --fail --silent --show-error \ -X POST "${LINKMESH_SERVER}/api/v1/bootstrap" \ -H "Content-Type: application/json" \ -d "{\"token\":\"${LINKMESH_TOKEN}\"}") echo "$RESP" | jq -r .data.caCertificate > /certs/ca.pem echo "$RESP" | jq -r .data.clientCertificate > /certs/client.pem echo "$RESP" | jq -r .data.clientKey > /certs/client-key.pem chown 65532:65532 /certs/*.pem chmod 644 /certs/ca.pem /certs/client.pem chmod 600 /certs/client-key.pem echo "Enrolled — collectorId=$(echo "$RESP" | jq -r .data.collectorId)" volumeMounts: - { name: certs, mountPath: /certs } containers: - name: agent image: docker.io/opensight/linkmesh-agent:latest imagePullPolicy: IfNotPresent securityContext: runAsUser: 65532 runAsGroup: 65532 runAsNonRoot: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumeMounts: - { name: config, mountPath: /etc/linkmesh-agent/config.yaml, subPath: config.yaml, readOnly: true } - { name: certs, mountPath: /etc/linkmesh-agent/certs, readOnly: true } resources: requests: { cpu: 25m, memory: 64Mi } limits: { cpu: 200m, memory: 256Mi } tolerations: - key: node-role.kubernetes.io/control-plane effect: NoSchedule volumes: - name: config configMap: name: linkmesh-agent-config items: - { key: config.yaml, path: config.yaml } - name: certs hostPath: path: /var/lib/linkmesh-agent/certs type: DirectoryOrCreate3. Substitute placeholders and apply
Three placeholders to fill in: your HTTPS host, your gRPC endpoint, the token from step 1.
export LINKMESH_SERVER=linkmesh.example.comexport LINKMESH_GRPC=grpc.linkmesh.example.com:50051export LINKMESH_TOKEN=<paste-token-from-wizard>
# Namespace firstkubectl apply -f 00-namespace.yaml
# Substitute and pipe to kubectlsed -e "s|__LINKMESH_SERVER__|${LINKMESH_SERVER}|g" \ -e "s|__LINKMESH_GRPC__|${LINKMESH_GRPC}|g" \ linkmesh-agent-configmap.yaml.tmpl | kubectl apply -f -
sed "s|__LINKMESH_TOKEN__|${LINKMESH_TOKEN}|g" \ linkmesh-agent-secret.yaml.tmpl | kubectl apply -f -
# DaemonSet — no placeholderskubectl apply -f linkmesh-agent-daemonset.yaml4. Verify
# Wait for pods on every node to come upkubectl -n linkmesh-system rollout status ds/linkmesh-agent
# Bootstrap init container logs — should show "Enrolled — collectorId=..."kubectl -n linkmesh-system logs -l app.kubernetes.io/name=linkmesh-agent \ -c bootstrap --tail=20
# Agent container logs — should show the gRPC connection coming upkubectl -n linkmesh-system logs -l app.kubernetes.io/name=linkmesh-agent \ -c agent --tail=20In your LinkMesh UI, open Collectors — there’s one entry per node, each Online and heartbeating.
Collector strategy on Kubernetes
The agent manages an OpenTelemetry collector — it doesn’t ingest telemetry
itself. On Linux hosts the agent installs upstream Grafana Alloy
(linkmesh-agent install alloy) and supervises it; on Kubernetes you wire up
the collector workload separately.
Two patterns work well:
Option A — Sidecar collector
Run the OpenTelemetry collector as a second container in the same pod as
the agent. The agent finds the collector over localhost, manages its config
lifecycle via the standard agent flow, and pushes config updates over the gRPC
stream like any other deployment.
Useful when you want tight 1:1 coupling between agent + collector and collector-config changes to redeploy as a unit.
Option B — Separate collector workload + OpAMP
Run Alloy or otelcol-contrib as its own DaemonSet (or Deployment) and enrol
it with LinkMesh over the OpenTelemetry Management Protocol.
The agent in this guide acts as the per-node heartbeat + cert-bearing presence;
the actual data plane lives in the collector pods.
Useful when you already have a collector-config tooling workflow (kustomize overlays, Helm values, OPA) and want LinkMesh as the management layer without disturbing it.
Pick whichever fits your team. The Collector concept page covers the trade-offs in more depth.
Production hardening
The defaults above are deliberately conservative but not maximally locked down. Before promoting to production:
- Pin the image tag to a specific version (e.g.
:1.25.2) instead of:latest. - Add a NetworkPolicy on the
linkmesh-systemnamespace allowing egress only to your LinkMesh server’s HTTPS + gRPC ports. - Replace
hostPathwith a per-node PVC if your storage class supports it —hostPathis convenient but every pod scheduled to that node has filesystem access to the cert directory. - Move the enrollment token into sealed-secrets, SOPS, or Vault rather than templating it directly into your overlay. Tokens are single-use and short-lived, but they’re still bootstrap credentials — treat them accordingly.
Trying it on kind
For a quick local evaluation:
# Create a kind clusterkind create cluster --name linkmesh-eval
# Run a LinkMesh server somewhere reachable from the kind cluster# (host.docker.internal works from kind pods on macOS/Windows)
# Then follow steps 1–4 above with:# LINKMESH_SERVER=host.docker.internal:8080# LINKMESH_GRPC=host.docker.internal:50051kind nodes share a Docker network, so any service reachable from your host on
host.docker.internal:<port> is reachable from the agent pods too.
Uninstall
kubectl delete ns linkmesh-system
# Optional: scrub the per-node cert dirs (one node at a time)for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do kubectl debug node/$node --image=alpine:3.20 -- \ sh -c "rm -rf /host/var/lib/linkmesh-agent"doneDeleting the namespace removes every workload but leaves the on-host cert dirs behind. Scrub them too if you’re decommissioning the cluster — or leave them if you’ll redeploy: the existing certs save a fresh enrollment round-trip.
Next steps
- Add a collector — the one-line installer flow for standalone Linux hosts (compare and contrast).
- Concepts → Collector — what a collector actually is + the managed-vs-Standards model.
- Build your first pipeline — wire your new K8s-enrolled collectors through a processing pipeline to a destination.
- Troubleshooting → Enrollment — bootstrap-init-container failures and how to read them.