| 1 | --- |
| 2 | title: Container images |
| 3 | description: 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 | |
| 6 | Every [Deployment](/kubernetes/workloads/deployments) and |
| 7 | [Job](/kubernetes/workloads/jobs) runs exactly one image source, set |
| 8 | flat 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 | |
| 17 | If several are present, `main` wins, then `image`, then |
| 18 | `context`/`dockerfile`. With `main`, `image` means something else. |
| 19 | See [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 |
| 25 | Docker 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 | |
| 36 | Without any of these, a `main` or `context` workload fails before |
| 37 | anything is applied, with a message asking for a `registry`. |
| 38 | |
| 39 | Images are tagged with a hash of their content. For `main`, that's |
| 40 | the bundled program and generated Dockerfile. For `context`, it's the |
| 41 | build context and Dockerfile. A deploy with unchanged content builds |
| 42 | nothing. |
| 43 | |
| 44 | ## Run a registry image |
| 45 | |
| 46 | ```typescript |
| 47 | const cache = yield* Kubernetes.Deployment("Cache", { |
| 48 | cluster, |
| 49 | image: "valkey/valkey:8.1", |
| 50 | port: 6379, |
| 51 | serviceType: "ClusterIP", |
| 52 | }); |
| 53 | ``` |
| 54 | |
| 55 | Alchemy decides whether to update from the reference string. |
| 56 | Re-pushing the same tag doesn't redeploy anything. Pin a version or a |
| 57 | digest such as `repo@sha256:…`, and change it to roll out a new |
| 58 | image. |
| 59 | |
| 60 | ## Build your Dockerfile |
| 61 | |
| 62 | Point `context` at a directory with a Dockerfile: |
| 63 | |
| 64 | ```typescript |
| 65 | const 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, |
| 73 | pass a path relative to the directory you deploy from. |
| 74 | Editing any file in the context triggers a rebuild and a rollout on |
| 75 | the next deploy. |
| 76 | |
| 77 | ## Bundle an Effect program |
| 78 | |
| 79 | With `main`, the workload's implementation is an Effect in the same |
| 80 | module: |
| 81 | |
| 82 | ```typescript |
| 83 | export 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 | |
| 94 | Alchemy bundles the module with rolldown and wraps it in a generated |
| 95 | entrypoint. For a Deployment the entrypoint is an HTTP server, and |
| 96 | for a Job it's a run-to-completion program. Alchemy then builds an |
| 97 | image 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 | |
| 112 | A change to the program, or to anything it imports, triggers a |
| 113 | rebuild and a rollout. |
| 114 | |
| 115 | ## CPU architecture |
| 116 | |
| 117 | Built images target the nodes' CPU architecture. Alchemy uses the |
| 118 | first 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 | |
| 127 | Registry images used as written are pulled by the nodes for their |
| 128 | own architecture, so prefer multi-architecture images. |
| 129 | |
| 130 | ## Pull from a private registry |
| 131 | |
| 132 | Nodes need credentials to pull private images, whether you pass them |
| 133 | as `image` or Alchemy pushed them from `main` or `context`. Create |
| 134 | an image-pull Secret with a [Manifest](/kubernetes/objects/manifests) |
| 135 | and reference it from the pod template: |
| 136 | |
| 137 | ```typescript |
| 138 | const 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 | |
| 149 | const 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 | |
| 162 | You can manage an image's name and tags yourself, for example to |
| 163 | publish `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 |
| 168 | import * as Docker from "alchemy/Docker"; |
| 169 | import * as Config from "effect/Config"; |
| 170 | |
| 171 | const 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 | |
| 182 | const api = yield* Kubernetes.Deployment("Api", { |
| 183 | cluster, |
| 184 | image: image.imageRef, |
| 185 | port: 8080, |
| 186 | }); |
| 187 | ``` |
| 188 | |
| 189 | Add `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. |