| 1 | --- |
| 2 | title: Setup |
| 3 | description: Install Alchemy and connect it to a Kubernetes cluster, whether a local cluster on your machine, any cluster in your kubeconfig, or an explicit connection for CI. Then add a registry for images built from your code. |
| 4 | --- |
| 5 | |
| 6 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 7 | import { effectVersion } from "../../../versions"; |
| 8 | |
| 9 | export const pkgs = `"alchemy@latest" "effect@${effectVersion}" "@effect/platform-bun@${effectVersion}" "@effect/platform-node@${effectVersion}"`; |
| 10 | |
| 11 | Alchemy deploys to any Kubernetes cluster whose API server it can |
| 12 | reach. That includes a cluster on your machine, GKE, AKS, EKS, |
| 13 | DigitalOcean, k3s, or an on-prem fleet. This page covers what you |
| 14 | need before the first deploy. You need the alchemy package, a |
| 15 | cluster to deploy to, a registry for images built from your code, |
| 16 | and permission to manage objects in the cluster. |
| 17 | |
| 18 | ## Prerequisites |
| 19 | |
| 20 | - [Bun](https://bun.sh) or Node.js 22+ |
| 21 | - [Docker](https://docs.docker.com/get-started/get-docker/), to build |
| 22 | images from your code and to run a local cluster |
| 23 | - A cluster. Use [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) |
| 24 | for a local one, or credentials for a hosted one. Hosted |
| 25 | credentials are usually a kubeconfig context that already works |
| 26 | with `kubectl`. |
| 27 | - [`helm`](https://helm.sh/docs/intro/install/), only if you install |
| 28 | [Helm charts](/kubernetes/objects/helm-charts) |
| 29 | - [`kubectl`](https://kubernetes.io/docs/tasks/tools/) is useful for |
| 30 | checking access and inspecting what Alchemy deploys. Alchemy talks |
| 31 | to the Kubernetes API directly and never runs it. |
| 32 | |
| 33 | ## Create a project directory |
| 34 | |
| 35 | <Tabs syncKey="pkgManager"> |
| 36 | <TabItem label="bun" icon="bun"> |
| 37 | <Code code="mkdir my-cluster && cd my-cluster && bun init -y" lang="sh" /> |
| 38 | </TabItem> |
| 39 | <TabItem label="npm" icon="npm"> |
| 40 | <Code code="mkdir my-cluster && cd my-cluster && npm init -y" lang="sh" /> |
| 41 | </TabItem> |
| 42 | <TabItem label="pnpm" icon="pnpm"> |
| 43 | <Code code="mkdir my-cluster && cd my-cluster && pnpm init" lang="sh" /> |
| 44 | </TabItem> |
| 45 | <TabItem label="yarn" icon="seti:yarn"> |
| 46 | <Code code="mkdir my-cluster && cd my-cluster && yarn init -y" lang="sh" /> |
| 47 | </TabItem> |
| 48 | </Tabs> |
| 49 | |
| 50 | ## Install |
| 51 | |
| 52 | <Tabs syncKey="pkgManager"> |
| 53 | <TabItem label="bun" icon="bun"> |
| 54 | <Code code={`bun add ${pkgs}`} lang="sh" /> |
| 55 | </TabItem> |
| 56 | <TabItem label="npm" icon="npm"> |
| 57 | <Code code={`npm install ${pkgs}`} lang="sh" /> |
| 58 | </TabItem> |
| 59 | <TabItem label="pnpm" icon="pnpm"> |
| 60 | <Code code={`pnpm add ${pkgs}`} lang="sh" /> |
| 61 | </TabItem> |
| 62 | <TabItem label="yarn" icon="seti:yarn"> |
| 63 | <Code code={`yarn add ${pkgs}`} lang="sh" /> |
| 64 | </TabItem> |
| 65 | </Tabs> |
| 66 | |
| 67 | ## Add the providers |
| 68 | |
| 69 | Every Stack that uses Kubernetes resources needs |
| 70 | `Kubernetes.providers()`: |
| 71 | |
| 72 | ```typescript |
| 73 | // alchemy.run.ts |
| 74 | import * as Alchemy from "alchemy"; |
| 75 | import * as Kubernetes from "alchemy/Kubernetes"; |
| 76 | import * as Effect from "effect/Effect"; |
| 77 | |
| 78 | export default Alchemy.Stack( |
| 79 | "MyApp", |
| 80 | { |
| 81 | providers: Kubernetes.providers(), |
| 82 | state: Alchemy.localState(), |
| 83 | }, |
| 84 | Effect.gen(function* () { |
| 85 | // Kubernetes resources go here |
| 86 | }), |
| 87 | ); |
| 88 | ``` |
| 89 | |
| 90 | It registers the `LocalCluster`, `Deployment`, `Job`, `Manifest`, and |
| 91 | `HelmChart` providers, plus authentication for kubeconfig, |
| 92 | bearer-token, client-certificate, and exec-plugin connections. There is no |
| 93 | `alchemy profile` step for Kubernetes. Credentials come from the |
| 94 | connection each resource carries. |
| 95 | |
| 96 | ## Connect to your cluster |
| 97 | |
| 98 | Each resource's `cluster` prop says where it runs and how Alchemy |
| 99 | authenticates. Find your kind of cluster below. |
| 100 | |
| 101 | ### A cluster on your machine |
| 102 | |
| 103 | If you don't have a cluster yet, let Alchemy create one. |
| 104 | [`Kubernetes.LocalCluster`](/kubernetes/clusters/local) runs a |
| 105 | [kind](https://kind.sigs.k8s.io) cluster in Docker with an image |
| 106 | registry built in: |
| 107 | |
| 108 | ```typescript |
| 109 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 110 | name: "alchemy", |
| 111 | }); |
| 112 | |
| 113 | const web = yield* Kubernetes.Deployment("Web", { |
| 114 | cluster, |
| 115 | image: "ghcr.io/stefanprodan/podinfo:6.15.0", |
| 116 | port: 9898, |
| 117 | serviceType: "ClusterIP", |
| 118 | }); |
| 119 | ``` |
| 120 | |
| 121 | Install kind first. On macOS run `brew install kind`, or see |
| 122 | [kind's install guide](https://kind.sigs.k8s.io/docs/user/quick-start/#installation). |
| 123 | The [tutorial](/kubernetes/tutorial/part-1) uses a local cluster |
| 124 | throughout. |
| 125 | |
| 126 | ### Any cluster with a kubeconfig |
| 127 | |
| 128 | If `kubectl` already works against the cluster, Alchemy can use the |
| 129 | same kubeconfig context. List your contexts and pick the one you |
| 130 | deploy to: |
| 131 | |
| 132 | ```sh |
| 133 | kubectl config get-contexts |
| 134 | ``` |
| 135 | |
| 136 | Pass its name in your program: |
| 137 | |
| 138 | ```typescript |
| 139 | const cluster = Kubernetes.KubeConfig({ context: "prod-us-east" }); |
| 140 | |
| 141 | const web = yield* Kubernetes.Deployment("Web", { |
| 142 | cluster, |
| 143 | image: "ghcr.io/acme/web:1.4.0", |
| 144 | port: 8080, |
| 145 | }); |
| 146 | ``` |
| 147 | |
| 148 | Alchemy reads the kubeconfig at deploy time. It uses `path` if you |
| 149 | pass one, otherwise the first entry of `$KUBECONFIG`, otherwise |
| 150 | `~/.kube/config`. Name the context explicitly, because the file's |
| 151 | `current-context` changes whenever someone runs |
| 152 | `kubectl config use-context`. |
| 153 | |
| 154 | Contexts created by a cloud CLI call an `exec` credential plugin for |
| 155 | short-lived tokens. The plugin must be installed and logged in on |
| 156 | every machine that deploys, including CI runners. |
| 157 | |
| 158 | ### GKE |
| 159 | |
| 160 | Write a context with the Google Cloud CLI: |
| 161 | |
| 162 | ```sh |
| 163 | gcloud container clusters get-credentials <cluster> --region <region> |
| 164 | ``` |
| 165 | |
| 166 | The context is named `gke_<project>_<region>_<cluster>` and calls |
| 167 | `gke-gcloud-auth-plugin`. Install the plugin with |
| 168 | `gcloud components install gke-gcloud-auth-plugin`. |
| 169 | |
| 170 | ### AKS |
| 171 | |
| 172 | Write a context with the Azure CLI: |
| 173 | |
| 174 | ```sh |
| 175 | az aks get-credentials --name <cluster> --resource-group <group> |
| 176 | ``` |
| 177 | |
| 178 | The context is named after the cluster. For clusters that use |
| 179 | Microsoft Entra ID authentication, install |
| 180 | [`kubelogin`](https://azure.github.io/kubelogin/) and convert the |
| 181 | context to use your Azure CLI login: |
| 182 | |
| 183 | ```sh |
| 184 | kubelogin convert-kubeconfig -l azurecli |
| 185 | ``` |
| 186 | |
| 187 | ### EKS |
| 188 | |
| 189 | EKS clusters can be created and targeted from the same Stack, with |
| 190 | ECR images and AWS bindings built in. See |
| 191 | [Amazon EKS](/kubernetes/clusters/eks). |
| 192 | |
| 193 | ### k3s, RKE2, and other distributions |
| 194 | |
| 195 | Self-managed distributions write a kubeconfig on the server. k3s |
| 196 | writes it to `/etc/rancher/k3s/k3s.yaml` and RKE2 writes it to |
| 197 | `/etc/rancher/rke2/rke2.yaml`. Copy it to the deploying machine and |
| 198 | change its `server` address from `127.0.0.1` to one your machine can |
| 199 | reach. Then point `path` at it: |
| 200 | |
| 201 | ```typescript |
| 202 | const cluster = Kubernetes.KubeConfig({ |
| 203 | path: "./k3s.yaml", |
| 204 | context: "default", |
| 205 | }); |
| 206 | ``` |
| 207 | |
| 208 | Hosted Kubernetes services such as DigitalOcean, Linode, and Scaleway |
| 209 | provide a kubeconfig from their console or CLI. Use it the same way. |
| 210 | |
| 211 | ### Existing local clusters |
| 212 | |
| 213 | A kind, minikube, or Docker Desktop cluster you started yourself adds |
| 214 | a context to `~/.kube/config`. The context is named `kind-<name>`, |
| 215 | `minikube`, or `docker-desktop`, and it works like any other |
| 216 | kubeconfig cluster. Local clusters usually have no load balancer, so |
| 217 | use `serviceType: "ClusterIP"` or `"NodePort"` for Deployments. |
| 218 | |
| 219 | ### Without a kubeconfig |
| 220 | |
| 221 | Some environments have no kubeconfig, such as a CI job or a |
| 222 | bootstrap script. There, describe the connection directly with an |
| 223 | endpoint and an `auth` descriptor: |
| 224 | |
| 225 | ```typescript |
| 226 | import * as Config from "effect/Config"; |
| 227 | |
| 228 | const token = yield* Config.String("K8S_TOKEN"); |
| 229 | |
| 230 | const cluster: Kubernetes.Connection = { |
| 231 | endpoint: "https://k8s.internal.example.com:6443", |
| 232 | certificateAuthorityData: "LS0tLS1CRUdJTi…", // base64-encoded PEM |
| 233 | auth: { kind: "token", token }, |
| 234 | }; |
| 235 | ``` |
| 236 | |
| 237 | `auth` can also be a client certificate (`kind: "client-cert"`) or an |
| 238 | exec plugin (`kind: "exec"`). A token or certificate in a raw |
| 239 | connection is stored in Alchemy's state. Changing it replaces the |
| 240 | resources that use it, so prefer kubeconfig or exec connections for |
| 241 | long-lived stacks. See |
| 242 | [Connecting to clusters](/kubernetes/clusters/connecting) for every |
| 243 | option. |
| 244 | |
| 245 | ## Check your permissions |
| 246 | |
| 247 | Alchemy needs RBAC permission to `get`, `create`, `patch`, and |
| 248 | `delete` the objects each resource manages, in the namespaces you |
| 249 | deploy to: |
| 250 | |
| 251 | - `Deployment` writes a ServiceAccount, a Deployment, and a Service. |
| 252 | - `Job` writes a ServiceAccount and a Job, or a CronJob when it has a |
| 253 | `schedule`. |
| 254 | - `Manifest` writes the one object in its `manifest`. |
| 255 | - `HelmChart` writes every object the chart renders, often including |
| 256 | cluster-scoped ClusterRoles, CRDs, and webhook configurations. |
| 257 | |
| 258 | Namespaces, ClusterRoles, CRDs, and other cluster-scoped kinds need |
| 259 | cluster-level permissions. Workload namespaces must already exist or |
| 260 | be created in the same Stack with a |
| 261 | [Manifest](/kubernetes/objects/manifests). |
| 262 | |
| 263 | Check before deploying with `kubectl auth can-i`, using the same |
| 264 | context: |
| 265 | |
| 266 | ```sh |
| 267 | kubectl --context prod-us-east auth can-i create deployments --namespace apps |
| 268 | kubectl --context prod-us-east auth can-i patch services --namespace apps |
| 269 | ``` |
| 270 | |
| 271 | ## Add a registry for your code |
| 272 | |
| 273 | Workloads that run a pre-built `image` need nothing else. Workloads |
| 274 | built from your code, such as an Effect program (`main`) or a |
| 275 | Dockerfile (`context`), are built on your machine. Alchemy pushes |
| 276 | them to a registry the cluster's nodes can pull from. Add one to the |
| 277 | connection: |
| 278 | |
| 279 | ```typescript |
| 280 | const cluster = Kubernetes.KubeConfig({ |
| 281 | context: "prod-us-east", |
| 282 | registry: { server: "ghcr.io/acme" }, |
| 283 | }); |
| 284 | ``` |
| 285 | |
| 286 | Alchemy pushes with your machine's Docker login unless you pass |
| 287 | credentials. `LocalCluster` and EKS provide a registry themselves. |
| 288 | See [Container registries](/kubernetes/clusters/registries). |
| 289 | |
| 290 | ## Deploy from CI |
| 291 | |
| 292 | A CI runner needs the same things as your laptop. It needs network |
| 293 | access to the API server, credentials, and any exec plugin your |
| 294 | context calls. Common setups: |
| 295 | |
| 296 | - Authenticate the runner to your cloud, for example with GitHub |
| 297 | OIDC. Then create the kubeconfig with the cloud CLI command above. |
| 298 | - Log the runner's Docker in to your registry (`docker login`) before |
| 299 | deploying workloads built from your code. |
| 300 | - Store a kubeconfig file as a secret, write it to disk, and set |
| 301 | `KUBECONFIG` to its path. |
| 302 | - Pass `path` and `context` from environment variables so the same |
| 303 | program targets a different cluster per stage. |
| 304 | |
| 305 | Use a remote [state store](/state-store) so CI and your team see the |
| 306 | same deployments. See [CI](/environments/ci) for running |
| 307 | `alchemy deploy` in GitHub Actions. |
| 308 | |
| 309 | ## State storage |
| 310 | |
| 311 | Kubernetes has no state backend of its own, so pick one of: |
| 312 | |
| 313 | - **`Alchemy.localState()`.** State lives on disk under `.alchemy/` |
| 314 | next to your code. This is fine for trying things out alone, but CI |
| 315 | and teammates can't see it. |
| 316 | - **`postgresState()`.** State lives in a Postgres database you |
| 317 | already run, such as a managed database from your Kubernetes |
| 318 | provider. It locks each stage while a deploy runs. See |
| 319 | [Postgres state store](/state-store#postgres-state-store). |
| 320 | - **`Cloudflare.state()` or `AWS.state()`.** State lives in your |
| 321 | Cloudflare or AWS account. See [State Store](/state-store). |
| 322 | - **Your own store.** Implement the state service over S3-compatible |
| 323 | storage, Redis, or anything else. See |
| 324 | [Custom state store](/state-store/custom-state-store). |
| 325 | |
| 326 | Each resource stores its cluster connection in state so |
| 327 | `alchemy destroy` can reach the cluster later, even after the code |
| 328 | that declared it is gone. |
| 329 | |
| 330 | ## Next steps |
| 331 | |
| 332 | - [Tutorial part 1](/kubernetes/tutorial/part-1) deploys a |
| 333 | Deployment, step by step. |
| 334 | - [Connecting to clusters](/kubernetes/clusters/connecting) covers |
| 335 | every connection form, multi-cluster stacks, and moving between |
| 336 | clusters. |
| 337 | - [Kubernetes overview](/kubernetes) is the map of resources and |
| 338 | guides. |