Skip to content

Part 1: Your First Deployment

In this first part you’ll create a Stack, start a Kubernetes cluster on your machine, and deploy a replicated HTTP service. You won’t write a line of YAML.

The tutorial runs on a local cluster so you can follow it without a cloud account. Everything you build works on any Kubernetes cluster. Part 5 shows how to point it at yours.

kind runs Kubernetes inside Docker containers. Alchemy runs it for you, but it must be installed. On macOS, run brew install kind.

Start with an empty directory and initialize a package.json:

Terminal window
mkdir my-cluster && cd my-cluster && bun init -y

Install alchemy@latest and effect@rc:

Terminal window
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"

Every Alchemy program starts with a Stack. A Stack is a collection of Resources, the providers that manage them, and a state store that remembers what was deployed. Create alchemy.run.ts:

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

Kubernetes.providers() teaches Alchemy how to create, update, and delete Kubernetes resources. Alchemy.localState() keeps state in a .alchemy/ directory next to your code.

Declare a cluster as the first resource:

Effect.gen(function* () {
// resources go here
const cluster = yield* Kubernetes.LocalCluster("Cluster", {
name: "alchemy",
});
}),

Kubernetes.LocalCluster is a resource like any other. Deploying creates a kind cluster named alchemy in Docker, and destroying deletes it. It also starts an image registry next to the cluster, which you’ll use in Part 3.

Declare a replicated HTTP server on the cluster. This one runs podinfo, a small demo service that answers with JSON:

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

cluster tells the Deployment where to run. Every Kubernetes resource takes one, and passing the LocalCluster makes the Deployment depend on it, so Alchemy creates the cluster first.

"Web" is the resource’s logical ID in your program. name: "web" is the name of the objects in the cluster. Without it, Alchemy generates one from the stack, stage, and logical ID, such as mycluster-web-live-you-…. Set it before the first deploy, because a Deployment keeps the name it was created with.

image is a registry reference the cluster pulls as written, and port is both the container port and the Service port.

serviceType defaults to LoadBalancer, which asks the cluster for an external load balancer and waits up to about three minutes for one to be assigned. A local cluster has no load balancer, so the example sets serviceType: "ClusterIP". The Service gets a cluster-internal address only, and you’ll reach it through kubectl port-forward.

Return the names you’ll need from the command line:

const web = yield* Kubernetes.Deployment("Web", {
cluster,
name: "web",
image: "ghcr.io/stefanprodan/podinfo:6.15.0",
port: 9898,
replicas: 2,
serviceType: "ClusterIP",
});
return {
context: cluster.context,
namespace: web.namespace,
service: web.serviceName,
};
}),

cluster.context is the kubeconfig context kind created for the cluster, which you’ll pass to kubectl.

Terminal window
bun alchemy deploy
Plan: 2 to create

+ Cluster (Kubernetes.LocalCluster)
+ Web (Kubernetes.Deployment)

Proceed?
◉ Yes ○ No
Creating kind cluster alchemy (about 30 seconds)...
Starting image registry on localhost:5001...
✓ Cluster (Kubernetes.LocalCluster) created
✓ Web (Kubernetes.Deployment) created
{
  context: "kind-alchemy",
  namespace: "default",
  service: "web",
}

Alchemy created the cluster, then applied three objects to it. They are a ServiceAccount, a Deployment with two replicas, and a Service in front of them. The deploy finishes as soon as the API server accepts the objects. Kubernetes pulls the image and starts the pods in the background.

kind added a kind-alchemy context to your kubeconfig. Wait for the rollout, then forward a local port to the Service:

Terminal window
kubectl --context kind-alchemy rollout status deployment/web
kubectl --context kind-alchemy port-forward service/web 9898:9898

In a second terminal:

Terminal window
curl localhost:9898
{
"hostname": "web-857dfd7659-7pfv2",
"version": "6.15.0",
"message": "greetings from podinfo v6.15.0",
...
}

Run alchemy deploy again. Nothing changed, so the plan is empty:

Plan: no changes

{
  context: "kind-alchemy",
  namespace: "default",
  service: "web",
}

This is the core loop. You declare resources in code, deploy, and Alchemy works out what changed.

You now have:

  • A Kubernetes cluster on your machine, declared as a resource
  • A two-replica podinfo Deployment behind a ClusterIP Service, created with no YAML
  • Stack outputs naming the kubeconfig context, the Service, and its namespace

In Part 2, you’ll give the app its own namespace and configure it with environment variables and resource limits.