1---
2title: Container images
3description: Choose where a Deployment or Job's container image comes from and where built images are pushed. Sources are a registry reference, your own Dockerfile, or a bundled Effect program.
4---
5
6Every [Deployment](/kubernetes/workloads/deployments) and
7[Job](/kubernetes/workloads/jobs) runs exactly one image source, set
8flat on its props:
9
10- **Registry reference.** `image` runs a pre-built image such as
11 `nginx:1.27`.
12- **Your Dockerfile.** `context` runs an image built from your
13 Dockerfile. `dockerfile` is optional.
14- **Effect program.** `main` runs your Effect program, bundled into a
15 generated image. `handler` and `build` are optional.
16
17If several are present, `main` wins, then `image`, then
18`context`/`dockerfile`. With `main`, `image` means something else.
19See [Bundle an Effect program](#bundle-an-effect-program).
20
21## Where built images go
22
23`image` references are pulled by the cluster's nodes as written.
24`context` and `main` images are built on the deploying machine with
25Docker and pushed to a registry the nodes can pull from:
26
27- **[`Kubernetes.LocalCluster`](/kubernetes/clusters/local).** Images
28 go to its built-in local registry.
29- **A connection with a `registry`.** Images go to
30 `<server>/<name>:<hash>` in that registry. See
31 [Container registries](/kubernetes/clusters/registries).
32- **[Amazon EKS](/kubernetes/clusters/eks).** Each workload gets a
33 private ECR repository. EKS also mirrors `image` references into
34 it.
35
36Without any of these, a `main` or `context` workload fails before
37anything is applied, with a message asking for a `registry`.
38
39Images are tagged with a hash of their content. For `main`, that's
40the bundled program and generated Dockerfile. For `context`, it's the
41build context and Dockerfile. A deploy with unchanged content builds
42nothing.
43
44## Run a registry image
45
46```typescript
47const cache = yield* Kubernetes.Deployment("Cache", {
48 cluster,
49 image: "valkey/valkey:8.1",
50 port: 6379,
51 serviceType: "ClusterIP",
52});
53```
54
55Alchemy decides whether to update from the reference string.
56Re-pushing the same tag doesn't redeploy anything. Pin a version or a
57digest such as `repo@sha256:…`, and change it to roll out a new
58image.
59
60## Build your Dockerfile
61
62Point `context` at a directory with a Dockerfile:
63
64```typescript
65const legacy = yield* Kubernetes.Deployment("Legacy", {
66 cluster,
67 context: "./legacy",
68 port: 8080,
69});
70```
71
72`dockerfile` defaults to `${context}/Dockerfile`. To use another one,
73pass a path relative to the directory you deploy from.
74Editing any file in the context triggers a rebuild and a rollout on
75the next deploy.
76
77## Bundle an Effect program
78
79With `main`, the workload's implementation is an Effect in the same
80module:
81
82```typescript
83export default Kubernetes.Deployment(
84 "Api",
85 { cluster, main: import.meta.url, port: 3000 },
86 Effect.gen(function* () {
87 return {
88 fetch: Effect.succeed(HttpServerResponse.text("ok")),
89 };
90 }),
91);
92```
93
94Alchemy bundles the module with rolldown and wraps it in a generated
95entrypoint. For a Deployment the entrypoint is an HTTP server, and
96for a Job it's a run-to-completion program. Alchemy then builds an
97image from a generated Dockerfile. The generated entrypoint imports
98`@effect/platform-bun`, so install it alongside `alchemy` and
99`effect`. The related props are:
100
101- `handler` is the named export to run. It defaults to `"default"`.
102- `image` is the base image of the generated Dockerfile. It defaults
103 to `oven/bun:1`. Any image that can run Bun works.
104- `dockerfile` is a base environment Dockerfile for when you need
105 system packages. It can be a path or inline content.
106- `build` sets bundler options. Unused code is tree-shaken, and
107 `effect`, alchemy, and `@distilled.cloud` are marked side-effect
108 free so they prune aggressively. Add your own side-effect-free
109 packages with `build: { pure: { packages: ["my-lib"] } }`. Turn
110 the behavior off with `build: { pure: false }`.
111
112A change to the program, or to anything it imports, triggers a
113rebuild and a rollout.
114
115## CPU architecture
116
117Built images target the nodes' CPU architecture. Alchemy uses the
118first of these that is set:
119
120- A workload's own `architecture` prop, either `"amd64"` or
121 `"arm64"`.
122- The connection's `architecture`. `LocalCluster` sets it to your
123 machine's, and you can set it on
124 [`KubeConfig`](/kubernetes/clusters/registries#match-the-node-architecture).
125- `amd64` when neither is set.
126
127Registry images used as written are pulled by the nodes for their
128own architecture, so prefer multi-architecture images.
129
130## Pull from a private registry
131
132Nodes need credentials to pull private images, whether you pass them
133as `image` or Alchemy pushed them from `main` or `context`. Create
134an image-pull Secret with a [Manifest](/kubernetes/objects/manifests)
135and reference it from the pod template:
136
137```typescript
138const pullSecret = yield* Kubernetes.Manifest("GhcrPullSecret", {
139 cluster,
140 manifest: {
141 apiVersion: "v1",
142 kind: "Secret",
143 type: "kubernetes.io/dockerconfigjson",
144 metadata: { name: "ghcr", namespace: "apps" },
145 data: { ".dockerconfigjson": dockerConfigJsonBase64 },
146 },
147});
148
149const api = yield* Kubernetes.Deployment("Api", {
150 cluster,
151 namespace: "apps",
152 image: "ghcr.io/acme/api:v1.4.0",
153 port: 8080,
154 podTemplate: {
155 spec: { imagePullSecrets: [{ name: pullSecret.name }] },
156 },
157});
158```
159
160## Build images with Docker.Image
161
162You can manage an image's name and tags yourself, for example to
163publish `acme/api:v1.4.0` for other systems too. Build it with
164[`Docker.Image`](/docker/build-and-push) and pass its reference as
165`image`:
166
167```typescript
168import * as Docker from "alchemy/Docker";
169import * as Config from "effect/Config";
170
171const image = yield* Docker.Image("ApiImage", {
172 name: "acme/api",
173 tag: "v1.4.0",
174 build: { context: "./api", platform: "linux/amd64" },
175 registry: {
176 server: "ghcr.io",
177 username: "acme-bot",
178 password: Config.Redacted("GHCR_TOKEN"),
179 },
180});
181
182const api = yield* Kubernetes.Deployment("Api", {
183 cluster,
184 image: image.imageRef,
185 port: 8080,
186});
187```
188
189Add `Docker.providers()` to the Stack's providers.
190
191## Where next
192
193- **[Container registries](/kubernetes/clusters/registries).** Add a
194 registry to a connection.
195- **[Jobs & CronJobs](/kubernetes/workloads/jobs).** Run Effect
196 programs to completion.
197- **[`Deployment` reference](/providers/kubernetes/deployment).**
198 Lists every image source prop.