1---
2title: "Part 5: Deploy to Your Own Cluster"
3description: Point the tutorial stack at a hosted Kubernetes cluster such as GKE, AKS, EKS, k3s, or anything in your kubeconfig. Keep the local cluster for development with stages, and clean up.
4sidebar:
5 order: 5
6---
7
8import Terminal from "../../../../components/Terminal.astro";
9import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
10
11Everything you built in Parts 1–4 runs on a cluster on your machine.
12None of it is specific to that cluster. The Deployment, the Effect
13Jobs, and the Helm chart only need a `cluster` to run on. In this part
14you'll point the stack at a cluster of your own, while keeping the
15local one for development.
16
17## Choose a cluster
18
19You need a cluster you can reach with `kubectl`. If you don't have
20one, create it with your platform's tools and follow its section of
21[Setup](/kubernetes/setup#connect-to-your-cluster):
22
23- **GKE.** `gcloud container clusters get-credentials` writes the
24 context. See [Setup → GKE](/kubernetes/setup#gke).
25- **AKS.** `az aks get-credentials` writes the context. See
26 [Setup → AKS](/kubernetes/setup#aks).
27- **k3s, RKE2, DigitalOcean, and others.** Use the kubeconfig the
28 platform gives you. See
29 [Setup → k3s, RKE2, and other distributions](/kubernetes/setup#k3s-rke2-and-other-distributions).
30- **Amazon EKS.** Create the cluster in this same Stack. See
31 [Amazon EKS](/kubernetes/clusters/eks).
32
33Then note its context name:
34
35```sh
36kubectl config get-contexts
37```
38
39The rest of this part uses a context named `prod`.
40
41## Choose a registry
42
43The smoke test and health check are built from your code, so they
44need a registry the cluster's nodes can pull from. The local cluster
45came with one. For yours, pick a repository you can push to, such as
46`ghcr.io/<you>` on GitHub, and log in:
47
48```sh
49echo $GITHUB_TOKEN | docker login ghcr.io -u <you> --password-stdin
50```
51
52Make the pushed images readable by the cluster. Use a public package,
53or [pull credentials](/kubernetes/clusters/registries#let-the-nodes-pull)
54for a private one. See [Container registries](/kubernetes/clusters/registries).
55
56## Pick the cluster by stage
57
58Every Alchemy deploy targets a **stage**. By default that's your own
59`live_<you>` stage, or you can name one with `--stage`. Keep the
60local cluster for your stage and use the hosted cluster for `prod`. In `src/infra.ts`,
61replace the `Cluster` declaration:
62
63```diff lang="typescript"
64+import { Stage } from "alchemy";
65import * as Kubernetes from "alchemy/Kubernetes";
66import * as Effect from "effect/Effect";
67
68-export const Cluster = Kubernetes.LocalCluster("Cluster", {
69- name: "alchemy",
70-});
71+export const Cluster = Effect.gen(function* () {
72+ const stage = yield* Stage;
73+ if (stage === "prod") {
74+ return Kubernetes.KubeConfig({
75+ context: "prod",
76+ registry: { server: "ghcr.io/you" },
77+ });
78+ }
79+ return yield* Kubernetes.LocalCluster("Cluster", { name: "alchemy" });
80+});
81```
82
83For `prod`, `Kubernetes.KubeConfig` connects through the `prod`
84context and pushes images to `ghcr.io/you`. Every other stage keeps
85the local cluster. Nothing else changes. The Namespace, Deployment,
86Jobs, and chart all take whichever cluster `Cluster` returns.
87
88## Set the node architecture
89
90Images are built for `amd64` nodes by default. If your cluster runs
91Arm nodes, such as AWS Graviton or Ampere, say so:
92
93```diff lang="typescript"
94return Kubernetes.KubeConfig({
95 context: "prod",
96 registry: { server: "ghcr.io/you" },
97+ architecture: "arm64",
98});
99```
100
101## Stop returning the local context
102
103`cluster.context` only exists on the local cluster. Remove it from
104the Stack's outputs in `alchemy.run.ts`:
105
106```diff lang="typescript"
107return {
108- context: cluster.context,
109 namespace: web.namespace,
110 service: web.serviceName,
111 smokeTest: smokeTest.jobName,
112};
113```
114
115## Deploy to prod
116
117<Tabs syncKey="pkgManager">
118 <TabItem label="bun" icon="bun">
119 <Code code="bun alchemy deploy --stage prod" lang="sh" />
120 </TabItem>
121 <TabItem label="npm" icon="npm">
122 <Code code="npm run alchemy deploy -- --stage prod" lang="sh" />
123 </TabItem>
124 <TabItem label="pnpm" icon="pnpm">
125 <Code code="pnpm alchemy deploy --stage prod" lang="sh" />
126 </TabItem>
127 <TabItem label="yarn" icon="seti:yarn">
128 <Code code="yarn alchemy deploy --stage prod" lang="sh" />
129 </TabItem>
130</Tabs>
131
132<Terminal content={`[u]Plan[/u]: [g]5 to create[/g]
133
134[g]+[/g] [b]HealthCheck[/b] [d](Kubernetes.Job)[/d]
135[g]+[/g] [b]MetricsServer[/b] [d](Kubernetes.HelmChart)[/d]
136[g]+[/g] [b]Namespace[/b] [d](Kubernetes.Manifest)[/d]
137[g]+[/g] [b]SmokeTest[/b] [d](Kubernetes.Job)[/d]
138[g]+[/g] [b]Web[/b] [d](Kubernetes.Deployment)[/d]
139
140Proceed?
141◉ Yes ○ No
142[d]Pushed ghcr.io/you/mycluster-smoketest-prod-…:7a31fba1…[/d]
143[d]Pushed ghcr.io/you/mycluster-healthcheck-prod-…:27bde98e…[/d]
144[g]✓[/g] [b]Namespace[/b] [d](Kubernetes.Manifest)[/d] [g]created[/g]
145[g]✓[/g] [b]MetricsServer[/b] [d](Kubernetes.HelmChart)[/d] [g]created[/g]
146[g]✓[/g] [b]Web[/b] [d](Kubernetes.Deployment)[/d] [g]created[/g]
147[g]✓[/g] [b]SmokeTest[/b] [d](Kubernetes.Job)[/d] [g]created[/g]
148[g]✓[/g] [b]HealthCheck[/b] [d](Kubernetes.Job)[/d] [g]created[/g]`} />
149
150`prod` has its own state, so this deploy creates everything on the
151hosted cluster and leaves your local stage alone. There's no
152`LocalCluster` in the plan, because the `prod` stage never declares
153one.
154
155## Check the hosted cluster
156
157The same commands work, with the `prod` context:
158
159```sh
160kubectl --context prod -n my-app get deployments,jobs,cronjobs
161kubectl --context prod -n my-app logs job/<smokeTest>
162```
163
164## Skip what the platform already has
165
166Many hosted clusters include metrics-server, and applying the chart
167over it would take ownership of its objects. Install the chart only
168where it's missing:
169
170```diff lang="typescript"
171+import { Stage } from "alchemy";
172import * as Kubernetes from "alchemy/Kubernetes";
173```
174
175```diff lang="typescript"
176yield* HealthCheck;
177
178-const metricsServer = yield* Kubernetes.HelmChart("MetricsServer", {
179- cluster,
180- // ...
181-});
182+const stage = yield* Stage;
183+if (stage !== "prod") {
184+ yield* Kubernetes.HelmChart("MetricsServer", {
185+ cluster,
186+ // ...
187+ });
188+}
189```
190
191Deploying `prod` again now plans the chart for deletion there.
192
193## Clean up the local cluster
194
195Destroy your own stage to remove everything on your machine,
196including the kind cluster and its registry:
197
198<Tabs syncKey="pkgManager">
199 <TabItem label="bun" icon="bun">
200 <Code code="bun alchemy destroy" lang="sh" />
201 </TabItem>
202 <TabItem label="npm" icon="npm">
203 <Code code="npm run alchemy destroy" lang="sh" />
204 </TabItem>
205 <TabItem label="pnpm" icon="pnpm">
206 <Code code="pnpm alchemy destroy" lang="sh" />
207 </TabItem>
208 <TabItem label="yarn" icon="seti:yarn">
209 <Code code="yarn alchemy destroy" lang="sh" />
210 </TabItem>
211</Tabs>
212
213Alchemy deletes the resources in reverse dependency order and the
214cluster last. The next `alchemy deploy` recreates it in about 30
215seconds. Run `alchemy destroy --stage prod` when you want to remove
216the hosted deployment too.
217
218## Recap
219
220You started with an empty directory and now have:
221
222- A stack that runs on a local cluster for development and on a
223 hosted cluster in `prod`
224- Effect programs built and pushed to whichever registry the cluster
225 uses
226- A Deployment, a Helm chart, and Jobs that don't depend on where
227 they run
228
229## Where next
230
231- **[Setup](/kubernetes/setup).** Connect to more kinds of clusters,
232 check RBAC permissions, and deploy from CI.
233- **[Container registries](/kubernetes/clusters/registries).** Push
234 credentials, pull access, and node architecture.
235- **[Deployments](/kubernetes/workloads/deployments).** Effect HTTP
236 servers, Service types, and the pod template escape hatch.
237- **[Jobs & CronJobs](/kubernetes/workloads/jobs).** Everything about
238 image and Effect Jobs.
239- **[Stages](/environments/stages).** Per-developer, preview, and
240 production stages.
241- **[Amazon EKS](/kubernetes/clusters/eks).** Create the cluster in
242 the same Stack and use AWS bindings.