Once a cluster has more than a couple of services talking to each other, a few questions start coming up that Kubernetes itself does not answer. Is traffic between my pods encrypted? Which service is calling which, and how often does it fail? If a request slows down, is it the network or the application code? You could answer these by adding a client library to every service, but that means touching every codebase, in every language, every time you want a new metric.
A service mesh solves this at the infrastructure layer instead. It attaches a small proxy to every pod, and that proxy handles encryption, retries, and metrics transparently, without your application code knowing it exists. Linkerd is the lightest and, by its own reputation, the least complicated of the major service meshes. Where Istio brings a large surface area of custom resources and an Envoy-based proxy, Linkerd ships a purpose-built micro-proxy written in Rust and keeps its feature set intentionally small: automatic mutual TLS, retries and timeouts, and rich traffic metrics, with very little configuration required to get real value from day one.
This tutorial is for developers and DevOps engineers already running a bare-metal Kubernetes cluster who want observability and encryption between services without a large operational burden. It builds directly on a few earlier guides: MetalLB for external IPs, Helm for installing charts, and the NGINX Ingress Controller for routing traffic in. If you already have a working cluster with kubectl and Helm configured, you are ready to start.
Conceptual Overview
Linkerd works around one core idea: the data plane proxy, called linkerd-proxy. When you enable Linkerd for a namespace or a workload, its admission webhook rewrites new pods to include this proxy as an extra container, a pattern called sidecar injection. Every network call your application makes, incoming or outgoing, is transparently routed through this proxy first.
Because every proxy in the mesh talks to every other proxy, and each proxy holds a workload identity issued by Linkerd’s built-in certificate authority, connections between meshed pods are automatically upgraded to mutual TLS (mTLS). Both sides prove their identity and encrypt the traffic, with no changes to application code and no certificates for you to manage by hand.
The control plane is a small set of pods (linkerd-destination, linkerd-identity, linkerd-proxy-injector) that runs the certificate authority, tells proxies where to route traffic, and watches for new pods to inject. Separately, the linkerd-viz extension adds Prometheus, a small web dashboard, and the linkerd viz CLI commands that show live golden-signal metrics (success rate, requests per second, latency percentiles) for any meshed workload, without you writing a single PromQL query by hand.
The mental model to keep: Linkerd does not replace your Ingress controller or your Service objects. It sits between them, wrapping the connections that already exist with encryption and visibility.
Prerequisites
- A working Kubernetes cluster (this guide assumes the bare-metal setup from the MetalLB and Ingress guides linked above), reachable with
kubectl get nodesreturningReadynodes. - Helm 3 installed and working; verify with
helm version. - At least 3 CPU cores and 4 GB of RAM free across the cluster for the control plane and its extensions, on top of whatever your workloads already use.
- A local Linux (Ubuntu) machine or workstation with
kubectlconfigured against the cluster, to run the installation from.
Step 1: Install the Linkerd CLI
Linkerd ships a CLI that handles installation, diagnostics, and day-to-day operations. Download and install it with the official install script:
curl -fsL https://run.linkerd.io/install-edge | sh
Add it to your PATH for the current shell and permanently in your shell profile:
export PATH=$PATH:$HOME/.linkerd2/bin
echo 'export PATH=$PATH:$HOME/.linkerd2/bin' >> ~/.bashrc
Confirm it works:
linkerd version --client
Client version: edge-24.9.4
The server side is not installed yet, that comes in a few steps.
Step 2: Run the Pre-Installation Check
Linkerd includes a check command specifically designed to catch cluster issues before you install anything. Run it now:
linkerd check --pre
You should see a series of checks ending in green checkmarks, finishing with:
Status check results are √
If anything fails here, for example missing RBAC permissions or an incompatible Kubernetes version, fix it before continuing. Installing on top of a failed pre-check almost always leads to a confusing half-broken control plane later.
Step 3: Install the Control Plane CRDs
Linkerd’s Custom Resource Definitions need to exist before the control plane itself. Install them with Helm:
helm repo add linkerd-edge https://helm.linkerd.io/edge
helm repo update
helm install linkerd-crds linkerd-edge/linkerd-crds \
--namespace linkerd \
--create-namespace
Step 4: Generate the Trust Anchor and Install the Control Plane
Linkerd’s mTLS relies on a trust anchor certificate that signs the identity certificates every proxy uses. For this tutorial we generate one locally with step, a small certificate tool:
curl -L https://github.com/smallstep/cli/releases/download/v0.25.2/step-cli_0.25.2_amd64.deb \
-o step-cli.deb
sudo dpkg -i step-cli.deb
Generate a self-signed trust anchor and an intermediate issuer certificate:
step certificate create root.linkerd.cluster.local ca.crt ca.key \
--profile root-ca --no-password --insecure
step certificate create identity.linkerd.cluster.local issuer.crt issuer.key \
--profile intermediate-ca --not-after 8760h --no-password --insecure \
--ca ca.crt --ca-key ca.key
Now install the control plane, passing in the certificates you just generated:
helm install linkerd-control-plane linkerd-edge/linkerd-control-plane \
--namespace linkerd \
--set-file identityTrustAnchorsPEM=ca.crt \
--set-file identity.issuer.tls.crtPEM=issuer.crt \
--set-file identity.issuer.tls.keyPEM=issuer.key
Give it a minute, then confirm every control plane pod is healthy:
linkerd check
A fully green result means the control plane, identity issuer, and proxy injector are all working correctly together.
Step 5: Install linkerd-viz for Dashboards and Metrics
The core control plane deliberately does not include a UI or Prometheus, keeping the base install lean. Add the viz extension with Helm:
helm install linkerd-viz linkerd-edge/linkerd-viz --namespace linkerd-viz --create-namespace
Verify it installed cleanly:
linkerd check
Then open the dashboard through a local port-forward:
linkerd viz dashboard
This opens a browser tab connected through a temporary kubectl port-forward, so it works even without an Ingress rule for it. Leave this running in a separate terminal while you continue, or press Ctrl+C and reopen it later; the underlying Grafana-free dashboard reads live from Prometheus each time.
Step 6: Deploy a Sample App and Inject the Proxy
Create a namespace for a small test workload:
kubectl create namespace demo
Deploy a simple two-tier example: a backend and a frontend that calls it. Save this as demo-app.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: demo
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: nginxdemos/hello:plain-text
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: demo
spec:
selector:
app: backend
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: curlimages/curl:8.9.1
command: ["sh", "-c", "while true; do curl -s http://backend; sleep 2; done"]
The key step is injecting the Linkerd proxy before applying it. You can pipe manifests through linkerd inject, which reads them and adds the sidecar container and annotations automatically:
kubectl apply -f <(linkerd inject demo-app.yaml)
Confirm both workloads now show 2/2 containers ready per pod (the app container plus linkerd-proxy):
kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
backend-7d4f9c8b7d-2xkpl 2/2 Running 0 40s
backend-7d4f9c8b7d-8mqzr 2/2 Running 0 40s
frontend-6b8d7f4c9d-vwq2t 2/2 Running 0 40s
2/2 is the signal you are looking for: your application container plus the injected proxy, both running.
Step 7: Verify mTLS and Watch Live Traffic
With the frontend continuously curling the backend, check live traffic statistics for the namespace:
linkerd viz stat deploy -n demo
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
backend 2/2 100.00% 0.5rps 3ms 6ms 8ms
frontend 1/1 100.00% 0.0rps 0ms 0ms 0ms
MESHED confirms every pod in the deployment is running the proxy, and the success rate and latency percentiles come from real traffic, with zero instrumentation added to either container image.
Now confirm the traffic between them is actually encrypted with mTLS, not just proxied:
linkerd viz edges deployment -n demo
SRC DST SRC_NS DST_NS SECURED
frontend backend demo demo √
The SECURED column showing a checkmark is the proof: Linkerd established mutual TLS between these two pods automatically, using the identities issued from the trust anchor you generated in Step 4.
Common Mistakes and Troubleshooting
Pods stay at 1/1 instead of 2/2 after kubectl apply. This almost always means the manifest was applied without going through linkerd inject first, so no sidecar was added. Re-apply using the linkerd inject pipe shown in Step 6, or add the namespace-wide annotation linkerd.io/inject: enabled on the namespace itself so future deployments are injected automatically.
linkerd check fails on identity or webhook checks. This is usually a certificate mismatch, often from mixing certificates generated in different runs of the step commands. Regenerate ca.crt/ca.key and issuer.crt/issuer.key together in one sequence, then reinstall the control plane cleanly rather than patching an existing install.
The proxy-injector silently does not inject new pods. Check that the namespace or pod actually carries the linkerd.io/inject: enabled annotation, and that the linkerd-proxy-injector pod in the linkerd namespace is Running. A crashed injector fails open by default, meaning pods start unmeshed rather than blocking deployment.
linkerd viz dashboard hangs or shows no data. Confirm linkerd-viz pods are healthy with kubectl get pods -n linkerd-viz, and that Prometheus has actually scraped a few cycles; give it thirty seconds after a fresh install before panicking about empty graphs.
Certificates expiring later in production. The intermediate issuer certificate generated in Step 4 was created with --not-after 8760h, one year. Track this expiry, Linkerd will start failing identity checks and mTLS handshakes if it lapses, and rotate it ahead of time using linkerd upgrade with fresh certificates.
Best Practices
Use step or cert-manager for certificate rotation in production. Manually generated certificates are fine for learning, but a real cluster should automate trust anchor rotation. Linkerd integrates with cert-manager, which you may already have installed for Ingress TLS, to issue and rotate the identity issuer certificate automatically.
Mesh namespaces deliberately, not globally. Annotate specific namespaces with linkerd.io/inject: enabled rather than meshing an entire cluster on day one. Start with one or two services, confirm behavior with linkerd viz stat, then expand.
Pair Linkerd with existing health checks. The proxy adds retries and timeouts on top of whatever Kubernetes already does with liveness and readiness probes. Review the earlier guide on Kubernetes health checks so the mesh’s retry behavior does not mask a genuinely unhealthy pod.
Watch the success rate metric as your default alert signal. linkerd viz stat surfaces success rate and latency percentiles per workload without any custom instrumentation. Wiring these into your existing monitoring is usually higher value than adding more application-level metrics first.
Keep resource requests realistic for the proxy. Every meshed pod gains a small extra container. It is lightweight, but at scale across hundreds of pods it adds up; account for it in your namespace resource planning alongside your application’s own limits.
Upgrade deliberately. Linkerd’s edge releases move fast. Pin a specific chart version in Helm rather than always pulling latest, and read release notes before upgrading a production control plane.
Conclusion
You installed the Linkerd service mesh on a bare-metal Kubernetes cluster: the CLI, the CRDs, a control plane secured with your own trust anchor, and the linkerd-viz extension for dashboards and metrics. You then injected the sidecar proxy into a real two-tier application and confirmed, with actual commands rather than trust alone, that traffic between the pods was both encrypted with mTLS and visible as live success-rate and latency metrics.
From here, a natural next step is meshing a real application namespace and wiring linkerd viz stat output into your existing monitoring stack. If you already run Prometheus and Grafana, Linkerd’s metrics can be scraped into the same dashboards you already watch. You might also explore Linkerd’s traffic-split feature for canary deployments once you are comfortable with the basics covered here. Either way, you now have real encryption and real visibility between your services, without a single line of application code changed.
</content>