#Build Command
Build your project for production. alepha build produces one dist/, carrying one server slice per runtime you asked for.
#Quick Start
alepha build # one node slice
alepha build --runtime node,workerd # both, from one build
Your production-ready app is now in the dist/ folder.
#One artifact, N runtimes
Releasing an app that runs on both Node and Cloudflare used to mean building twice. Both runs rebuilt the client bundle, re-prerendered and re-compressed the assets, and those are the slow steps: only the server link genuinely differs, and it differs by a handful of export conditions.
So the client bundle, the prerender, the compression, the headers, the PWA manifest and the build manifest all run once, and only the server link repeats. Measured on Lore: 15.2s for a workerd-only build against 15.4s for node,workerd, of which build client is 3.9s.
⚠️ Declared order is meaningful. The first runtime is the primary: it is manifest.runtime, it is what dist/package.json's main points at, and it is what a deployer spawns. ["bun", "node"] and ["node", "bun"] produce the same two slices and different behaviour.
Then three commands turn that one dist/ into the format you need:
| format | command | runtimes |
|---|---|---|
| binary | alepha compile |
bun |
| archive | alepha pack |
node, bun, workerd |
| image | alepha image |
node, bun, and bun via --compile for a small image |
⚠️ There is no --target. The build is described by what it produces: declaring a workerd slice is what writes the Cloudflare config, runtime: ["static"] is what makes a static site, and Docker is its own command.
#What It Does
The build runs a fixed pipeline of tasks:
- Cleans the dist folder: Fresh start, no stale files
- Builds the client: Compiles React, bundles assets, optimizes for browsers
- Builds the server: one slice per declared runtime, and only this step repeats
- Copies assets: Moves static files to the right places
- Generates the PWA manifest: If
pwais configured - Prerenders pages: Sitemap and static pages, when applicable
- Generates deployment configs:
wrangler.jsoncwhen a workerd slice was built - Pre-compresses assets: Writes
.br(Brotli) copies of client assets
#Output Structure
After building, your dist/ folder looks like this:
dist/
├── index.node.js # one entry wrapper per slice
├── index.workerd.js
├── server/
│ ├── node/ # each slice's chunks, namespaced
│ └── workerd/
├── public/ # static assets (CSS, JS, images)
│ ├── entry.abc123.js
│ ├── chunk.def456.js
│ └── favicon.svg
├── manifest.json # what the build declared; `alepha pack` requires it
└── package.json # `main` points at the primary slice
⚠️ The slices are namespaced, and that is load-bearing. Two runtimes built into one server/ do not collide - their content hashes differ - so both sets would simply sit there, and the wrangler server/*.js glob would sweep the Node chunks into the Worker upload: a Worker twice the size it needs, or a Node chunk importing a node builtin and a refused deploy.
⚠️ There is no index.js that works out its host. The manifest is the discovery mechanism, because a second one able to disagree with it is worse than none.
Run your server with:
node dist # resolves the primary slice through `main`
node dist/index.node.js # or name the slice
#Options
| Flag | Description |
|---|---|
--runtime, -r |
Runtimes to link the server for, comma-separated and in order: node, bun, workerd. The first is the primary. static declares an app with no server. |
--stats |
Generate build statistics report (use --stats=json for JSON output) |
--prebuilt |
Skip the bundle steps; only regenerate the target-specific deploy config (e.g. wrangler.jsonc) when dist/ is already built |
Declaring a workerd slice is what writes the Cloudflare deploy config; runtime: ["static"] is what makes a static site. There is no --target: the build is described by what it produces.
#Deployment Targets
#Standard Node.js Server
alepha build
Deploy anywhere that runs Node.js:
# Copy dist/ to your server
scp -r dist/ user@server:/app
# On the server
cd /app && node index.js
#Docker
alepha build && alepha image
alepha image generates a Dockerfile in the app directory when there is none, beside alepha.config.ts, and reuses yours when there is: the file is meant to be committed and edited. It is not written into dist/, which the build wipes on every run.
alepha image --tag # tag:latest
alepha image --tag=1.3.4 # tag:1.3.4
The generated image runs as uid 1000, not root (image.user overrides it). image.env and image.volumes bake ENV defaults and VOLUME declarations into it, so a self-contained image needs no docker run flags - see the Docker deployment guide.
alepha image --compile compiles the bun slice first and ships an image holding the binary and nothing else, on gcr.io/distroless/cc-debian12 (image.from overrides it). That variant stays root: the base has no shell to prepare a volume with.
⚠️ A Bun --compile binary is not static, whatever triple it is built for: it needs an interpreter, libstdc++ and libgcc. alepha image derives the triple from the base for that reason, and refuses a base with no libc (scratch, distroless/static) rather than producing a container that exits immediately.
⚠️ alepha image shells out to docker build, so it is a local and CI command. It cannot run in an in-process build path.
#Cloudflare Workers
alepha build --runtime=workerd # or -t cf
Creates Cloudflare Workers configuration:
dist/
├── main.cloudflare.js # Worker entry point
├── wrangler.jsonc # Wrangler configuration
├── manifest.json # Build manifest (detected resources, declared env keys)
└── public/ # Static assets (if any)
D1 Database Support
If your
DATABASE_URLuses thed1://protocol (as injected by the platform plugin), the D1 binding is automatically configured inwrangler.jsonc.
Then deploy:
cd dist && wrangler deploy
Or let alepha p up drive the whole pipeline - provisioning, build, migrations, deploy, and secrets.
#Static Site
alepha build --runtime=static
Prerenders your pages to plain HTML/CSS/JS for any static host. Not compatible with --prebuilt (prerendering needs a live app).
#SEO Features
#Sitemap Generation
Add the $sitemap primitive to a router. It
serves sitemap.xml from your $page primitives - live at request time, and
prerendered to dist/public/sitemap.xml at build time (so static deployments get
the file too):
1import { $sitemap } from "alepha/react/sitemap";2 3class AppRouter {4 // hostname defaults to PUBLIC_URL, then "" (relative URLs)5 sitemap = $sitemap({ hostname: "https://myapp.com" });6}
Produces dist/public/sitemap.xml with all your routes:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://myapp.com/</loc>
<lastmod>2026-06-06</lastmod>
</url>
<url>
<loc>https://myapp.com/about</loc>
<lastmod>2026-06-06</lastmod>
</url>
<!-- ... -->
</urlset>
#Build Statistics
alepha build --stats
Generates interactive reports about your bundles:
# View client bundle analysis
open dist/public/stats.html
# View server bundle analysis
open dist/stats.html
The reports show:
- File sizes (before and after compression)
- Chunk breakdown
- Dependency analysis
Bundle Analysis
Use this to find large dependencies dragging down your bundle size.
#Environment Variables
The build process respects your environment:
API_URL=https://api.myapp.com
Build-time Variables
Environment variables are embedded at build time. For variables that should differ between environments, use runtime configuration instead.
#Build Configuration
Defaults for the build command live in the build section of alepha.config.ts. Command-line flags override them:
1import { defineConfig } from "alepha/cli/config"; 2 3export default defineConfig({ 4 build: { 5 runtime: "bun", 6 stats: true, 7 compile: "myapp", 8 docker: { 9 image: { tag: "ghcr.io/myorg/myapp", oci: true },10 },11 pwa: {12 name: "My App",13 themeColor: "#0f172a",14 },15 },16});
Available options mirror the flags (stats, target, runtime, compile) plus per-target configuration. compile takes true, a binary name, or { name, target, minify } for the Bun target triple and minification:
| Section | Description |
|---|---|
output |
Override dist and public directory names |
cloudflare |
Extra wrangler.jsonc config merged into the generated file |
docker |
Base image, run command, global installs, baked env/volumes/user, image tag/args/OCI labels |
static |
Surge domain for the CNAME file; source to adopt a client directory the workspace built itself (must live outside dist/) |
pwa |
Web app manifest: name, short name, colors, display mode |
#Client-Side Optimization
The build automatically:
- Minifies JavaScript: Removes whitespace, shortens variable names
- Minifies CSS: Combines and compresses styles
- Tree shakes: Removes unused code
- Code splits: Creates separate chunks for routes
- Hashes filenames: Enables aggressive caching
- Pre-compresses assets: Writes Brotli (
.br) copies of JS/CSS/SVG/HTML
#Server-Side Optimization
The server build:
- Bundles dependencies: Single file, no
node_modulesneeded in production - Externalizes Node built-ins: Uses native
fs,path, etc. - Preserves source maps: Debug production issues when needed
Single File Deploy
Your production server is a single
index.jsfile. No need to deploynode_modules- everything is bundled.
#Backend-Only Projects
If your project has no browser entry, the build only creates the server:
alepha build
# → dist/index.js (your server/CLI/worker)
Perfect for:
- API servers
- CLI tools
- Background workers
- Scheduled jobs
#Build Workflow
A typical deployment workflow:
# 1. Verify everything works
alepha verify
# 2. Build for production
alepha build --runtime=workerd
# 3. Deploy
alepha platform up --env production
#Tips
Run verify first. The alepha verify command catches issues before you build. Don't ship broken code.
Check bundle sizes. Run alepha build --stats periodically. Large bundles slow down your users.
Test the production build locally. After building, run node dist/index.js locally before deploying. Catch issues early.
Environment variables matter. Make sure your production .env is correct. Wrong API URLs are a common deployment bug.