1---
2title: Local clusters
3description: Run a Kubernetes cluster on your machine with Kubernetes.LocalCluster. It is a kind cluster in Docker with an image registry wired into its nodes, so Effect programs and Dockerfiles deploy the same way they do on a hosted cluster.
4---
5
6[`Kubernetes.LocalCluster`](/providers/kubernetes/localcluster) runs
7a Kubernetes cluster on your machine. Use it to try Alchemy's
8Kubernetes resources, develop against a real cluster without paying
9for one, or run tests.
10
11```typescript
12const cluster = yield* Kubernetes.LocalCluster("Cluster", {
13 name: "alchemy",
14});
15
16const web = yield* Kubernetes.Deployment("Web", {
17 cluster,
18 image: "ghcr.io/stefanprodan/podinfo:6.15.0",
19 port: 9898,
20 serviceType: "ClusterIP",
21});
22```
23
24Unlike [`Kubernetes.KubeConfig`](/kubernetes/clusters/connecting),
25which describes a cluster that already exists, `LocalCluster` is a
26resource. Deploying creates the cluster, and destroying deletes it.
27
28## What it creates
29
30A `LocalCluster` is two Docker containers:
31
32- a single-node [kind](https://kind.sigs.k8s.io) cluster named
33 `name`, and
34- a `registry:3` image registry named `<name>-registry`,
35 published on `localhost:<registryPort>`.
36
37Alchemy connects the registry to the cluster's Docker network and
38configures the node to pull `localhost:<registryPort>/…` images from
39it, following kind's
40[local registry](https://kind.sigs.k8s.io/docs/user/local-registry/)
41setup. Creating the cluster takes about 30 seconds.
42
43## Requirements
44
45- [Docker](https://docs.docker.com/get-started/get-docker/) must be
46 running.
47- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
48 must be installed. On macOS, run `brew install kind`. Set `KIND_BIN`
49 to use a binary that isn't on `PATH`.
50
51## Pass it as the cluster
52
53The resource's `connection` attribute carries everything a workload
54needs, so pass the whole resource as `cluster`:
55
56- **Authentication.** It uses the kubeconfig context `kind-<name>`.
57- **Registry.** It points at `localhost:<registryPort>`, so `main`
58 programs and `context` Dockerfiles are built and pushed there.
59- **Architecture.** It matches your Docker host's CPU architecture,
60 so images are built for the node without setting `architecture`.
61
62Effect programs therefore run without extra setup:
63
64```typescript
65const hello = yield* Kubernetes.Job(
66 "Hello",
67 { cluster, main: import.meta.url },
68 Effect.gen(function* () {
69 return {
70 run: Effect.log("hello from a Job"),
71 };
72 }),
73);
74```
75
76## Use kubectl
77
78kind adds the context `kind-<name>` to your kubeconfig, so `kubectl`
79works against the cluster directly:
80
81```sh
82kubectl --context kind-alchemy get pods --all-namespaces
83```
84
85Set `kubeconfig` to write the context to a different file instead
86of `$KUBECONFIG` or `~/.kube/config`:
87
88```typescript
89const cluster = yield* Kubernetes.LocalCluster("Cluster", {
90 name: "alchemy",
91 kubeconfig: "./.alchemy/kubeconfig",
92});
93```
94
95## Reach services
96
97A kind cluster has no cloud load balancer. Use
98`serviceType: "ClusterIP"` or `"NodePort"` for Deployments, and
99reach them with `kubectl port-forward`:
100
101```sh
102kubectl --context kind-alchemy port-forward service/web 9898:9898
103```
104
105A `LoadBalancer` Service would wait about three minutes for an
106address that never arrives.
107
108## Expose services on localhost
109
110To reach a service without `port-forward`, forward a host port to the
111node. This maps `localhost:8080` to port 30080 on the node:
112
113```typescript
114const cluster = yield* Kubernetes.LocalCluster("Cluster", {
115 name: "alchemy",
116 nodes: [
117 {
118 role: "control-plane",
119 extraPortMappings: [{ containerPort: 30080, hostPort: 8080 }],
120 },
121 ],
122});
123```
124
125Then give the service a `NodePort` Service on that port. A Deployment
126labels its pods `app.kubernetes.io/name: <name>`, so a
127[Manifest](/kubernetes/objects/manifests) can select them:
128
129```typescript
130const web = yield* Kubernetes.Deployment("Web", {
131 cluster,
132 name: "web",
133 image: "ghcr.io/stefanprodan/podinfo:6.15.0",
134 port: 9898,
135 serviceType: "ClusterIP",
136});
137
138yield* Kubernetes.Manifest("WebNodePort", {
139 cluster,
140 manifest: {
141 apiVersion: "v1",
142 kind: "Service",
143 metadata: { name: "web-nodeport", namespace: web.namespace },
144 spec: {
145 type: "NodePort",
146 selector: { "app.kubernetes.io/name": web.deploymentName },
147 ports: [{ port: 9898, targetPort: 9898, nodePort: 30080 }],
148 },
149 },
150});
151```
152
153`curl localhost:8080` now reaches podinfo. The same approach puts an
154ingress controller or Gateway implementation on ports 80 and 443:
155map those ports on the node, then install the controller with a
156[Helm chart](/kubernetes/objects/helm-charts), configured for kind.
157
158## Configure the cluster
159
160`LocalCluster` accepts the fields of a
161[kind cluster configuration](https://kind.sigs.k8s.io/docs/user/configuration/)
162as props: `nodes` with their `extraPortMappings`, `extraMounts`, and
163labels, `networking`, `featureGates`, `runtimeConfig`, and the kubeadm
164and containerd patches. Alchemy adds the containerd setting its
165registry needs.
166
167Worker nodes pull from the registry too:
168
169```typescript
170const cluster = yield* Kubernetes.LocalCluster("Cluster", {
171 name: "alchemy",
172 nodes: [{ role: "control-plane" }, { role: "worker" }, { role: "worker" }],
173});
174```
175
176kind can't change a running cluster, so changing any of these fields
177replaces the cluster.
178
179## Choose a Kubernetes version
180
181`nodeImage` selects the kind node image, and with it the Kubernetes
182version:
183
184```typescript
185const cluster = yield* Kubernetes.LocalCluster("Cluster", {
186 name: "alchemy",
187 nodeImage: "kindest/node:v1.33.1",
188});
189```
190
191Without it, kind uses its default node image. Changing `nodeImage`
192replaces the cluster.
193
194## Run several clusters
195
196Each cluster needs its own registry port:
197
198```typescript
199const blue = yield* Kubernetes.LocalCluster("Blue", { name: "blue" });
200const green = yield* Kubernetes.LocalCluster("Green", {
201 name: "green",
202 registryPort: 5002,
203});
204```
205
206`registryPort` defaults to `5001`. Changing it recreates the registry
207container on the new port, which drops the images pushed to it.
208Workloads on the cluster are updated in the same deploy and push
209their images to the new port.
210
211## Updates and replacement
212
213- Changing `name`, `nodeImage`, `kubeconfig`, or any kind field
214 (`nodes`, `networking`, and so on) replaces the cluster.
215 Alchemy deletes the cluster and creates a new one. Everything
216 deployed to it is recreated on the new cluster.
217- Changing `registryPort` moves the registry to the new port.
218
219If you delete the cluster outside Alchemy, for example with
220`kind delete cluster`, [`alchemy drift`](/cli/drift) reports it as
221missing. Run
222`alchemy deploy --force` to recreate it and apply everything on it
223again.
224
225## Take it to a real cluster
226
227`LocalCluster` is for local work. The same workloads run on a hosted
228cluster by changing what `cluster` points at. See
229[Deploy to your own cluster](/kubernetes/tutorial/part-5).
230
231## Where next
232
233- **[Tutorial](/kubernetes/tutorial/part-1).** Build up a stack on a
234 local cluster step by step.
235- **[Container registries](/kubernetes/clusters/registries).** Learn
236 about the `registry` setting `LocalCluster` fills in for you.
237- **[`LocalCluster` reference](/providers/kubernetes/localcluster).**
238 Look up every prop and attribute.