1---
2title: Deployments
3description: Run a replicated server on any Kubernetes cluster with Kubernetes.Deployment. Covers names, namespaces, Service types and URLs, replicas, resources, labels, the pod template escape hatch, and Effect servers.
4---
5
6[`Kubernetes.Deployment`](/providers/kubernetes/deployment) is a
7replicated server, running a container image or an HTTP server
8written in Effect. One resource applies three Kubernetes objects:
9
10- a **ServiceAccount** the pods run as,
11- a **Deployment** that keeps `replicas` copies of the container
12 running, and
13- a **Service** that gives them one stable address.
14
15```typescript
16const web = yield* Kubernetes.Deployment("Web", {
17 cluster,
18 name: "web",
19 namespace: "apps",
20 image: "ghcr.io/stefanprodan/podinfo:6.15.0",
21 port: 9898,
22 replicas: 3,
23 serviceType: "ClusterIP",
24});
25```
26
27## Names
28
29`name` is the name of all three objects. If you omit it, Alchemy
30generates one from the stack, stage, and logical ID, lowercased and
31limited to characters Kubernetes accepts. Either way the name is
32fixed at creation. Later changes to `name` are ignored, which keeps
33the Service address stable for everything that calls it.
34
35Every object also carries the label
36`app.kubernetes.io/name: <name>`, which the Service uses to select
37the pods.
38
39## Namespaces
40
41`namespace` defaults to `default`. The namespace must already exist.
42Create it in the same Stack with a
43[Manifest](/kubernetes/objects/manifests) and pass the Manifest's
44`name` attribute so Alchemy orders the Deployment after it:
45
46```typescript
47const ns = yield* Kubernetes.Manifest("AppsNamespace", {
48 cluster,
49 manifest: { apiVersion: "v1", kind: "Namespace", metadata: { name: "apps" } },
50});
51
52const web = yield* Kubernetes.Deployment("Web", {
53 cluster,
54 namespace: ns.name,
55 image: "ghcr.io/stefanprodan/podinfo:6.15.0",
56 port: 9898,
57});
58```
59
60Changing the namespace moves the workload. Alchemy applies the
61objects in the new namespace and deletes them from the old one.
62
63## Ports and Services
64
65`port` (default `3000`) is the container port and the Service port.
66A Deployment exposes one port. `serviceType` picks how the Service
67is reachable:
68
69- **`"LoadBalancer"`** is the default. The Service is reachable from
70 outside the cluster through a cloud load balancer. `url` is
71 `http://<hostname or IP>[:port]`.
72- **`"NodePort"`** is reachable on each node's IP at a port
73 Kubernetes assigns. `url` is `undefined`.
74- **`"ClusterIP"`** is reachable inside the cluster only. `url` is
75 `undefined`.
76
77For a `LoadBalancer` Service, the deploy waits up to about three
78minutes for the cluster to publish the load balancer's hostname or
79IP, then sets `url`. The port is part of the URL unless it's `80`.
80If no address appears in time, the deploy still succeeds and `url`
81is `undefined`. The next update picks it up.
82
83Clusters without a load balancer controller never assign an
84address. That includes kind, minikube, and most bare-metal clusters.
85Use `ClusterIP` or `NodePort` there, or install a controller such as
86[cloud-provider-kind](https://github.com/kubernetes-sigs/cloud-provider-kind)
87or [MetalLB](https://metallb.io). Inside the cluster, a Service is
88reachable at `http://<serviceName>.<namespace>.svc.cluster.local:<port>`.
89
90Pass `serviceAnnotations` for load balancer settings such as scheme
91or target type:
92
93```typescript
94serviceAnnotations: {
95 "service.beta.kubernetes.io/aws-load-balancer-scheme": "internal",
96},
97```
98
99Some platforms set their own defaults for `LoadBalancer` Services.
100On [Amazon EKS](/kubernetes/clusters/eks), the default is an
101internet-facing Network Load Balancer.
102
103## Replicas, resources, command, and args
104
105```typescript
106const api = yield* Kubernetes.Deployment("Api", {
107 cluster,
108 image: "ghcr.io/acme/api:v3",
109 port: 8080,
110 replicas: 3,
111 resources: {
112 requests: { cpu: "250m", memory: "256Mi" },
113 limits: { memory: "512Mi" },
114 },
115 command: ["/app/server"],
116 args: ["--log-level", "info"],
117});
118```
119
120`replicas` defaults to `1`. `resources` uses Kubernetes quantities.
121`command` overrides the image's entrypoint and `args` its arguments.
122
123## Environment variables
124
125`env` sets container environment variables. Values can be Outputs
126from other resources, and non-string values are JSON-encoded. Alchemy
127also sets `PORT`, `ALCHEMY_STACK_NAME`, `ALCHEMY_STAGE`, and
128`ALCHEMY_PHASE`. Your `env` wins on conflicts. See
129[Configuration & bindings](/kubernetes/workloads/bindings).
130
131## Labels
132
133`labels` are merged over the generated `app.kubernetes.io/name`
134label and applied to the Deployment, its pods, and the Service. They
135are also part of the Deployment's pod selector:
136
137```typescript
138labels: { "app.kubernetes.io/part-of": "shop", tier: "frontend" },
139```
140
141Kubernetes doesn't allow a Deployment's selector to change, so set
142`labels` before the first deploy. Changing them later fails with
143`spec.selector: … field is immutable`. To change them anyway, give
144the resource a new logical ID and a new `name`. Alchemy creates the
145new Deployment and deletes the old one. For labels that shouldn't be
146part of the selector, set them on the pod template instead, as
147described in the next section.
148
149## Tune the pod template
150
151`podTemplate` is an escape hatch for pod-level settings Alchemy
152doesn't model. It's a partial Kubernetes
153[`PodTemplateSpec`](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-template-v1/#PodTemplateSpec)
154merged over the generated one. Objects merge key by key, while
155arrays and primitive values replace what was there.
156
157```typescript
158const api = yield* Kubernetes.Deployment("Api", {
159 cluster,
160 image: "ghcr.io/acme/api:v3",
161 port: 8080,
162 podTemplate: {
163 metadata: {
164 annotations: { "prometheus.io/scrape": "true" },
165 },
166 spec: {
167 nodeSelector: { "kubernetes.io/arch": "arm64" },
168 tolerations: [{ key: "dedicated", operator: "Exists" }],
169 terminationGracePeriodSeconds: 30,
170 },
171 },
172});
173```
174
175Good fits are pod metadata, `nodeSelector`, `affinity`,
176`tolerations`, `topologySpreadConstraints`, `priorityClassName`,
177pod `securityContext`, `imagePullSecrets`, and `volumes`.
178Container-level fields live inside the `containers` array. That
179includes probes, `volumeMounts`, `envFrom`, and a sidecar. An array
180in `podTemplate` replaces the generated one wholesale, including the
181image. When a workload needs full control of its containers, write
182it as a [Manifest](/kubernetes/objects/manifests).
183
184## Effect servers
185
186With `main`, the Deployment runs an Effect program instead of an
187image. Alchemy bundles the module, builds an image, and serves the
188returned `fetch` handler on `port`:
189
190```typescript
191// src/Api.ts
192export default Kubernetes.Deployment(
193 "Api",
194 Effect.gen(function* () {
195 const cluster = yield* Cluster;
196 return {
197 cluster,
198 main: import.meta.url,
199 name: "api",
200 port: 3000,
201 replicas: 2,
202 serviceType: "ClusterIP" as const,
203 };
204 }),
205 Effect.gen(function* () {
206 return {
207 fetch: Effect.gen(function* () {
208 const request = yield* HttpServerRequest;
209 return HttpServerResponse.text(`hello from ${request.url}`);
210 }),
211 };
212 }),
213);
214```
215
216The image is pushed to the cluster's registry.
217[`LocalCluster`](/kubernetes/clusters/local) includes one. Other
218clusters need a [`registry`](/kubernetes/clusters/registries) on the
219connection, and EKS uses ECR. See
220[Container images](/kubernetes/workloads/images).
221
222The tagged form declares the Deployment as a class, so other code
223can depend on it by type and the implementation is provided as a
224Layer:
225
226```typescript
227export class Api extends Kubernetes.Deployment<Api>()("Api") {}
228
229export default Api.make(
230 { cluster, main: import.meta.url, port: 3000 },
231 Effect.gen(function* () {
232 return {
233 fetch: Effect.gen(function* () {
234 return HttpServerResponse.text("ok");
235 }),
236 };
237 }),
238);
239```
240
241In the Stack, `yield* Api` and provide the default export with
242`Effect.provide(ApiLive)`. This is the same authoring model as
243[Lambda functions](/aws/compute/lambda) and
244[Cloudflare Workers](/cloudflare/compute/workers).
245
246## What a deploy waits for
247
248A deploy finishes when the API server has accepted the objects and,
249for `LoadBalancer` Services, when the load balancer address is known.
250It doesn't wait for the rollout. Pods may still be pulling images or
251failing health checks. Check with `kubectl rollout status`, or add a
252smoke-test [Job](/kubernetes/workloads/jobs) that calls the Service.
253
254## Updates and replacement
255
256- **`image`, `env`, `replicas`, `resources`, `podTemplate`, and other
257 props.** Changing these is an update. The objects are re-applied
258 and Kubernetes rolls the pods.
259- **Files under `context`, or the `main` program.** Changing these is
260 an update, detected from the image content hash.
261- **`namespace`.** The objects move to the new namespace.
262- **`cluster`.** Moving to a different connection is a replacement.
263 See
264 [Move a resource to another cluster](/kubernetes/clusters/connecting#move-a-resource-to-another-cluster).
265- **`labels`.** Kubernetes rejects the change. See [Labels](#labels).
266- **`name`.** Changes are ignored after creation.
267
268## Attributes
269
270- `deploymentName` is the name of the Kubernetes Deployment.
271- `serviceName` is the name of the Service.
272- `serviceAccountName` is the name of the ServiceAccount the pods
273 run as.
274- `namespace` is the namespace of all three objects.
275- `port` is the container and Service port.
276- `url` is the load balancer URL for `LoadBalancer` Services, and
277 `undefined` otherwise.
278- `imageUri` is the image the pods run.
279- `connection` is the cluster connection.
280- `kubernetesObjects` holds references to the applied objects.
281
282## Where next
283
284- **[Container images](/kubernetes/workloads/images).** Covers
285 `image`, `context`, and `main` on each kind of cluster.
286- **[Jobs & CronJobs](/kubernetes/workloads/jobs).** Run-to-completion
287 work with the same props.
288- **[`Deployment` reference](/providers/kubernetes/deployment).**
289 Lists every prop and attribute.