alepha@docs:~/docs/.content/release/docs/cli/commands$
cat 8-pack.md | pretty
3 min read
Last commit:

#Pack Command

Pack a built workspace into a deployable tar.zst. The archive contains everything a remote runner needs to deploy the app - Alepha Bay, Alepha Rocket, or alepha deploy --prebuilt in the app's checkout - and nothing else.

#Quick Start

bash
alepha build --runtime node,workerd
alepha pack --tag 0.0.2
# → my-app-0.0.2.tar.zst

#What's Inside

The build's contents are at the archive root. There is no dist/ wrapper:

txt
public/               client bundle and prerendered HTML
server/node/          one chunk directory per server slice
server/workerd/
index.node.js         one entry wrapper per slice
index.workerd.js
manifest.json         what the build declared
migrations/           SQL migration files (if present)

No source, no alepha.config.ts, no package.json - the deploy side reads everything it needs from manifest.json (which slices are inside, detected resources, declared env keys, platform options) and never touches source. Excluded: node_modules, .alepha build cache, e2e, playwright-report, coverage, and OS junk files.

#Why zstd, and why the window is pinned

A multi-slice artifact holds near-identical copies of the same bundle, megabytes apart. DEFLATE's match window is 32 KB, so gzip never sees the second copy and the archive is simply twice the size. zstd with long-range matching does see it, but only if its window spans the distance between the two.

Measured on a real two-slice build of Lore, 61 MB uncompressed:

one slice two slices ratio
gzip -9 1.72 MB 3.44 MB 2.00x
zstd, default window 1.45 MB 2.88 MB 1.99x
zstd, pinned window (32 MiB) 1.45 MB 1.47 MB 1.02x

At the default window nothing fails: the archive is just twice the size it should be, at gzip-like ratios with extra machinery. The window is pinned explicitly and the ratio is asserted in a spec, because a pinned value alone drifts as apps outgrow it and an assertion alone never sets it.

Source maps are excluded from the artifact and written to a sibling <project>-<tag>.maps.tar.zst, rooted the same way so a map sits beside the chunk it describes.

#Options

Flag Description
--tag, -t Tag suffix for the artifact name, Docker-style (default: latest → <project>-latest.tar.zst)
--output, -o Output directory for the archive (default: current directory)
--name, -n Project name for the artifact filename (default: package.json name)

The project name comes from package.json name, slugified for the filename (@acme/app → acme-app).

--name is what a deploy passes when the app is known by another name: infra({ name }) sets the identity the deploy side uses, and it is free to differ from the package name. The Bay adapter of alepha deploy passes it for you (alepha pack --name <project>), so pack writes the file the deploy then looks for. Unlike the package.json default it is used verbatim, so it has to be a plain filename: letters, digits, ., _ and -.

#Deploying a Packed Artifact

alepha deploy --prebuilt reads its environments from alepha.config.ts: an environment names an adapter class, which no manifest can carry. Extract the archive into the dist/ of a checkout that holds that config:

bash
mkdir -p dist
tar -xf my-app-0.0.2.tar.zst -C dist
alepha deploy --prebuilt

--prebuilt skips the Vite bundle steps and reads the detected resources from dist/manifest.json, so the app's source is never imported. Only the deploy config (wrangler.jsonc) is regenerated.

Extracted on its own, the archive has no environments, and alepha deploy --prebuilt stops on "Missing infra configuration". An orchestrator that deploys the bare artifact (Alepha Rocket, Lore's deploy runner) supplies the environments itself. This is the workflow those orchestrators run: build once, pack once, deploy the same artifact to many environments or tenants.