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.
Prerequisites
Section titled “Prerequisites”kind runs Kubernetes inside Docker containers. Alchemy runs it for
you, but it must be installed. On macOS, run brew install kind.
Create a project
Section titled “Create a project”Start with an empty directory and initialize a package.json:
mkdir my-cluster && cd my-cluster && bun init -ymkdir my-cluster && cd my-cluster && npm init -ymkdir my-cluster && cd my-cluster && pnpm initmkdir my-cluster && cd my-cluster && yarn init -yInstall dependencies
Section titled “Install dependencies”Install alchemy@latest and effect@rc:
bun add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"npm install "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"pnpm add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"yarn add "alchemy@latest" "effect@rc" "@effect/platform-bun@rc" "@effect/platform-node@rc"Create the Stack
Section titled “Create the Stack”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:
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.
Start a local cluster
Section titled “Start a local cluster”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.
Add a Deployment
Section titled “Add a Deployment”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.
Keep the Service internal
Section titled “Keep the Service internal”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 Stack outputs
Section titled “Return Stack outputs”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.
Deploy
Section titled “Deploy”bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployPlan: 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.
Verify it worked
Section titled “Verify it worked”kind added a kind-alchemy context to your kubeconfig. Wait for the
rollout, then forward a local port to the Service:
kubectl --context kind-alchemy rollout status deployment/webkubectl --context kind-alchemy port-forward service/web 9898:9898In a second terminal:
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
ClusterIPService, 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.