Skip to content

Part 5: Deploy to Your Own Cluster

Everything you built in Parts 1–4 runs on a cluster on your machine. None of it is specific to that cluster. The Deployment, the Effect Jobs, and the Helm chart only need a cluster to run on. In this part you’ll point the stack at a cluster of your own, while keeping the local one for development.

You need a cluster you can reach with kubectl. If you don’t have one, create it with your platform’s tools and follow its section of Setup:

Then note its context name:

Terminal window
kubectl config get-contexts

The rest of this part uses a context named prod.

The smoke test and health check are built from your code, so they need a registry the cluster’s nodes can pull from. The local cluster came with one. For yours, pick a repository you can push to, such as ghcr.io/<you> on GitHub, and log in:

Terminal window
echo $GITHUB_TOKEN | docker login ghcr.io -u <you> --password-stdin

Make the pushed images readable by the cluster. Use a public package, or pull credentials for a private one. See Container registries.

Every Alchemy deploy targets a stage. By default that’s your own live_<you> stage, or you can name one with --stage. Keep the local cluster for your stage and use the hosted cluster for prod. In src/infra.ts, replace the Cluster declaration:

import { Stage } from "alchemy";
import * as Kubernetes from "alchemy/Kubernetes";
import * as Effect from "effect/Effect";
export const Cluster = Kubernetes.LocalCluster("Cluster", {
name: "alchemy",
});
export const Cluster = Effect.gen(function* () {
const stage = yield* Stage;
if (stage === "prod") {
return Kubernetes.KubeConfig({
context: "prod",
registry: { server: "ghcr.io/you" },
});
}
return yield* Kubernetes.LocalCluster("Cluster", { name: "alchemy" });
});

For prod, Kubernetes.KubeConfig connects through the prod context and pushes images to ghcr.io/you. Every other stage keeps the local cluster. Nothing else changes. The Namespace, Deployment, Jobs, and chart all take whichever cluster Cluster returns.

Images are built for amd64 nodes by default. If your cluster runs Arm nodes, such as AWS Graviton or Ampere, say so:

return Kubernetes.KubeConfig({
context: "prod",
registry: { server: "ghcr.io/you" },
architecture: "arm64",
});

cluster.context only exists on the local cluster. Remove it from the Stack’s outputs in alchemy.run.ts:

return {
context: cluster.context,
namespace: web.namespace,
service: web.serviceName,
smokeTest: smokeTest.jobName,
};
Terminal window
bun alchemy deploy --stage prod
Plan: 5 to create

+ HealthCheck (Kubernetes.Job)
+ MetricsServer (Kubernetes.HelmChart)
+ Namespace (Kubernetes.Manifest)
+ SmokeTest (Kubernetes.Job)
+ Web (Kubernetes.Deployment)

Proceed?
◉ Yes ○ No
Pushed ghcr.io/you/mycluster-smoketest-prod-…:7a31fba1…
Pushed ghcr.io/you/mycluster-healthcheck-prod-…:27bde98e…
✓ Namespace (Kubernetes.Manifest) created
✓ MetricsServer (Kubernetes.HelmChart) created
✓ Web (Kubernetes.Deployment) created
✓ SmokeTest (Kubernetes.Job) created
✓ HealthCheck (Kubernetes.Job) created

prod has its own state, so this deploy creates everything on the hosted cluster and leaves your local stage alone. There’s no LocalCluster in the plan, because the prod stage never declares one.

The same commands work, with the prod context:

Terminal window
kubectl --context prod -n my-app get deployments,jobs,cronjobs
kubectl --context prod -n my-app logs job/<smokeTest>

Many hosted clusters include metrics-server, and applying the chart over it would take ownership of its objects. Install the chart only where it’s missing:

import { Stage } from "alchemy";
import * as Kubernetes from "alchemy/Kubernetes";
yield* HealthCheck;
const metricsServer = yield* Kubernetes.HelmChart("MetricsServer", {
cluster,
// ...
});
const stage = yield* Stage;
if (stage !== "prod") {
yield* Kubernetes.HelmChart("MetricsServer", {
cluster,
// ...
});
}

Deploying prod again now plans the chart for deletion there.

Destroy your own stage to remove everything on your machine, including the kind cluster and its registry:

Terminal window
bun alchemy destroy

Alchemy deletes the resources in reverse dependency order and the cluster last. The next alchemy deploy recreates it in about 30 seconds. Run alchemy destroy --stage prod when you want to remove the hosted deployment too.

You started with an empty directory and now have:

  • A stack that runs on a local cluster for development and on a hosted cluster in prod
  • Effect programs built and pushed to whichever registry the cluster uses
  • A Deployment, a Helm chart, and Jobs that don’t depend on where they run
  • Setup. Connect to more kinds of clusters, check RBAC permissions, and deploy from CI.
  • Container registries. Push credentials, pull access, and node architecture.
  • Deployments. Effect HTTP servers, Service types, and the pod template escape hatch.
  • Jobs & CronJobs. Everything about image and Effect Jobs.
  • Stages. Per-developer, preview, and production stages.
  • Amazon EKS. Create the cluster in the same Stack and use AWS bindings.