Skip to content

Setup

Alchemy deploys to any Kubernetes cluster whose API server it can reach. That includes a cluster on your machine, GKE, AKS, EKS, DigitalOcean, k3s, or an on-prem fleet. This page covers what you need before the first deploy. You need the alchemy package, a cluster to deploy to, a registry for images built from your code, and permission to manage objects in the cluster.

  • Bun or Node.js 22+
  • Docker, to build images from your code and to run a local cluster
  • A cluster. Use kind for a local one, or credentials for a hosted one. Hosted credentials are usually a kubeconfig context that already works with kubectl.
  • helm, only if you install Helm charts
  • kubectl is useful for checking access and inspecting what Alchemy deploys. Alchemy talks to the Kubernetes API directly and never runs it.
Terminal window
mkdir my-cluster && cd my-cluster && bun init -y
Terminal window
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"

Every Stack that uses Kubernetes resources needs Kubernetes.providers():

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Kubernetes from "alchemy/Kubernetes";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyApp",
{
providers: Kubernetes.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
// Kubernetes resources go here
}),
);

It registers the LocalCluster, Deployment, Job, Manifest, and HelmChart providers, plus authentication for kubeconfig, bearer-token, client-certificate, and exec-plugin connections. There is no alchemy profile step for Kubernetes. Credentials come from the connection each resource carries.

Each resource’s cluster prop says where it runs and how Alchemy authenticates. Find your kind of cluster below.

If you don’t have a cluster yet, let Alchemy create one. Kubernetes.LocalCluster runs a kind cluster in Docker with an image registry built in:

const cluster = yield* Kubernetes.LocalCluster("Cluster", {
name: "alchemy",
});
const web = yield* Kubernetes.Deployment("Web", {
cluster,
image: "ghcr.io/stefanprodan/podinfo:6.15.0",
port: 9898,
serviceType: "ClusterIP",
});

Install kind first. On macOS run brew install kind, or see kind’s install guide. The tutorial uses a local cluster throughout.

If kubectl already works against the cluster, Alchemy can use the same kubeconfig context. List your contexts and pick the one you deploy to:

Terminal window
kubectl config get-contexts

Pass its name in your program:

const cluster = Kubernetes.KubeConfig({ context: "prod-us-east" });
const web = yield* Kubernetes.Deployment("Web", {
cluster,
image: "ghcr.io/acme/web:1.4.0",
port: 8080,
});

Alchemy reads the kubeconfig at deploy time. It uses path if you pass one, otherwise the first entry of $KUBECONFIG, otherwise ~/.kube/config. Name the context explicitly, because the file’s current-context changes whenever someone runs kubectl config use-context.

Contexts created by a cloud CLI call an exec credential plugin for short-lived tokens. The plugin must be installed and logged in on every machine that deploys, including CI runners.

Write a context with the Google Cloud CLI:

Terminal window
gcloud container clusters get-credentials <cluster> --region <region>

The context is named gke_<project>_<region>_<cluster> and calls gke-gcloud-auth-plugin. Install the plugin with gcloud components install gke-gcloud-auth-plugin.

Write a context with the Azure CLI:

Terminal window
az aks get-credentials --name <cluster> --resource-group <group>

The context is named after the cluster. For clusters that use Microsoft Entra ID authentication, install kubelogin and convert the context to use your Azure CLI login:

Terminal window
kubelogin convert-kubeconfig -l azurecli

EKS clusters can be created and targeted from the same Stack, with ECR images and AWS bindings built in. See Amazon EKS.

Self-managed distributions write a kubeconfig on the server. k3s writes it to /etc/rancher/k3s/k3s.yaml and RKE2 writes it to /etc/rancher/rke2/rke2.yaml. Copy it to the deploying machine and change its server address from 127.0.0.1 to one your machine can reach. Then point path at it:

const cluster = Kubernetes.KubeConfig({
path: "./k3s.yaml",
context: "default",
});

Hosted Kubernetes services such as DigitalOcean, Linode, and Scaleway provide a kubeconfig from their console or CLI. Use it the same way.

A kind, minikube, or Docker Desktop cluster you started yourself adds a context to ~/.kube/config. The context is named kind-<name>, minikube, or docker-desktop, and it works like any other kubeconfig cluster. Local clusters usually have no load balancer, so use serviceType: "ClusterIP" or "NodePort" for Deployments.

Some environments have no kubeconfig, such as a CI job or a bootstrap script. There, describe the connection directly with an endpoint and an auth descriptor:

import * as Config from "effect/Config";
const token = yield* Config.String("K8S_TOKEN");
const cluster: Kubernetes.Connection = {
endpoint: "https://k8s.internal.example.com:6443",
certificateAuthorityData: "LS0tLS1CRUdJTi…", // base64-encoded PEM
auth: { kind: "token", token },
};

auth can also be a client certificate (kind: "client-cert") or an exec plugin (kind: "exec"). A token or certificate in a raw connection is stored in Alchemy’s state. Changing it replaces the resources that use it, so prefer kubeconfig or exec connections for long-lived stacks. See Connecting to clusters for every option.

Alchemy needs RBAC permission to get, create, patch, and delete the objects each resource manages, in the namespaces you deploy to:

  • Deployment writes a ServiceAccount, a Deployment, and a Service.
  • Job writes a ServiceAccount and a Job, or a CronJob when it has a schedule.
  • Manifest writes the one object in its manifest.
  • HelmChart writes every object the chart renders, often including cluster-scoped ClusterRoles, CRDs, and webhook configurations.

Namespaces, ClusterRoles, CRDs, and other cluster-scoped kinds need cluster-level permissions. Workload namespaces must already exist or be created in the same Stack with a Manifest.

Check before deploying with kubectl auth can-i, using the same context:

Terminal window
kubectl --context prod-us-east auth can-i create deployments --namespace apps
kubectl --context prod-us-east auth can-i patch services --namespace apps

Workloads that run a pre-built image need nothing else. Workloads built from your code, such as an Effect program (main) or a Dockerfile (context), are built on your machine. Alchemy pushes them to a registry the cluster’s nodes can pull from. Add one to the connection:

const cluster = Kubernetes.KubeConfig({
context: "prod-us-east",
registry: { server: "ghcr.io/acme" },
});

Alchemy pushes with your machine’s Docker login unless you pass credentials. LocalCluster and EKS provide a registry themselves. See Container registries.

A CI runner needs the same things as your laptop. It needs network access to the API server, credentials, and any exec plugin your context calls. Common setups:

  • Authenticate the runner to your cloud, for example with GitHub OIDC. Then create the kubeconfig with the cloud CLI command above.
  • Log the runner’s Docker in to your registry (docker login) before deploying workloads built from your code.
  • Store a kubeconfig file as a secret, write it to disk, and set KUBECONFIG to its path.
  • Pass path and context from environment variables so the same program targets a different cluster per stage.

Use a remote state store so CI and your team see the same deployments. See CI for running alchemy deploy in GitHub Actions.

Kubernetes has no state backend of its own, so pick one of:

  • Alchemy.localState(). State lives on disk under .alchemy/ next to your code. This is fine for trying things out alone, but CI and teammates can’t see it.
  • postgresState(). State lives in a Postgres database you already run, such as a managed database from your Kubernetes provider. It locks each stage while a deploy runs. See Postgres state store.
  • Cloudflare.state() or AWS.state(). State lives in your Cloudflare or AWS account. See State Store.
  • Your own store. Implement the state service over S3-compatible storage, Redis, or anything else. See Custom state store.

Each resource stores its cluster connection in state so alchemy destroy can reach the cluster later, even after the code that declared it is gone.