alepha@docs:~/framework/docs/cli/commands$
cat 10-image.md | pretty
4 min read
Last commit:

#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
 1import { defineConfig } from "alepha/cli/config"; 2  3export default defineConfig({ 4  image: { 5    from: "node:26-alpine", 6    volumes: ["/data"], 7    env: { DATA_DIR: "/data" }, 8    tag: "ghcr.io/myorg/app", 9    oci: true,10  },11});

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