# Image Command

Build a container image from `./dist`.

## Quick Start

```bash
alepha build
alepha image --tag
# → <image.tag>:latest
```

⚠️ **It needs the docker CLI.** It shells out to `docker build`, so it is a local and CI command and cannot run in an in-process build path.

## A Dockerfile you own

No Dockerfile in the app directory: one is generated there, beside `alepha.config.ts`. One already present: it is used untouched.

⚠️ It lands in the **app directory**, not in `dist/`, and that is not a preference: `alepha build` wipes `dist/` on every run, so a file living there could never be committed or edited. The build **context** is still `dist/`, so the Dockerfile's `COPY . .` keeps meaning the built output rather than dragging `src/` and `node_modules/` in.

The generated header records the manifest facts it came from:

```dockerfile
# Generated by `alepha image`, from this build's manifest:
#   runtime: node
#   entry: index.node.js
# It is yours now. Edit it freely; `alepha image` reuses it untouched
# and only warns when the facts above stop matching a later build.
```

A later run **warns** when they no longer match, and never fails. The file is yours after generation, so a hard failure would make an intentional edit feel like a bug; the warning exists only so the drift is not silent.

## Which slice it runs

The primary: the first declared runtime, read from the manifest. An image runs one process from one entry point, and this is the same rule every deployer follows.

A `static` artifact is refused (it runs no process, so there is nothing to start) and so is a `workerd` one (only Cloudflare runs it).

## A binary and nothing else

```bash
alepha image --compile --tag
```

Compiles the bun slice first and ships an image holding the binary, on `gcr.io/distroless/cc-debian12`. Measured on a real app: **197 MB against 279 MB**, and 49 MB against 67 MB compressed.

### ⚠️ The libc, which is where this goes wrong

A Bun `--compile` binary is **not static**, under any triple:

```txt
$ file dist/app
ELF 64-bit LSB executable, dynamically linked,
interpreter /lib/ld-musl-aarch64.so.1
$ ldd dist/app
Error loading shared library libstdc++.so.6: No such file or directory
Error loading shared library libgcc_s.so.1: No such file or directory
```

It needs an interpreter, `libstdc++` and `libgcc`. Put it on a base that has none and you do not get a build failure: you get `exec /app/app: no such file or directory`, naming a file that is plainly there, from a container that simply stops.

So `alepha image` derives the triple from the base rather than inheriting whatever the host would default to, and **refuses a base with no libc** (`scratch`, `distroless/static`) rather than building one, because no triple fixes it.

The compiled variant stays root: the base has no shell, so a declared volume cannot be prepared at build time.

## Options

| Flag              | Description                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `--tag`, `-t`     | `--tag` uses `latest`, `--tag=1.3.4` that version with the configured name, `--tag=other/img:v1` verbatim |
| `--compile`, `-c` | Compile the bun slice first and ship an image holding the binary. `--compile name` names it               |
| `--dockerfile`    | Write the Dockerfile and stop, without building an image                                                  |

Building needs a name: `image.tag` in the config, unless `--tag` is a complete `name:tag`. `--tag=:1.3.4` is the same as `--tag=1.3.4`.

## Configuration

`image:` is a **top-level** key in `alepha.config.ts`, not `build.docker`: config follows the command that reads it.

```typescript
import { defineConfig } from "alepha/cli/config";

export default defineConfig({
  image: {
    from: "node:26-alpine",
    volumes: ["/data"],
    env: { DATA_DIR: "/data" },
    tag: "ghcr.io/myorg/app",
    oci: true,
  },
});
```

`build.cloudflare` correctly stays under `build`, because `wrangler.jsonc` really is written by the build.

These keys shape the generated Dockerfile. A Dockerfile you already own is used untouched, so they only reach it through a regeneration (delete it and run `alepha image` again). `tag`, `args` and the build labels of `oci` go on the `docker build` command, so they apply either way.

| Key       | Default                                                                                       | Description                                                                                                                                                                      |
| --------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from`    | `node:24-alpine`, `oven/bun:alpine` for bun, `gcr.io/distroless/cc-debian12` with `--compile` | Base image (`FROM`)                                                                                                                                                              |
| `command` | `node`, or `bun` for bun                                                                      | Command that runs the entry. Not used with `--compile`, where the binary is the command                                                                                          |
| `install` | -                                                                                             | Packages installed with `npm install` before the app is copied, for CLI tools the app shells out to (e.g. `wrangler`). Ignored with `--compile`                                  |
| `env`     | -                                                                                             | `ENV` defaults, emitted after the built-in `SERVER_HOST=0.0.0.0`. Not secrets: `docker run -e` overrides them and `docker inspect` shows them                                    |
| `volumes` | -                                                                                             | `VOLUME` mount points. Each is created and owned by the container user first, so a named volume starts writable                                                                  |
| `user`    | `1000` (none with `--compile`)                                                                | `USER` the process runs as. `"root"` opts back into root                                                                                                                         |
| `tag`     | -                                                                                             | Image name without a version, completed by `--tag`                                                                                                                               |
| `args`    | -                                                                                             | Extra `docker build` arguments, e.g. `["--platform", "linux/amd64"]`                                                                                                             |
| `oci`     | -                                                                                             | Add the OCI labels: `revision` (git commit), `created` and `version` (from the tag) on the build command, plus the `labels` below                                                |
| `labels`  | -                                                                                             | `source`, `title`, `description` and `licenses` OCI labels, written into the Dockerfile when `oci` is on, each only when set. `source` links a package to its repository on GHCR |
