Context
Today the layered image build (#263) ships one rootfs flavor per preset: Ubuntu 24.04 with Node 22, Python 3, uv, git, openssh. Base rootfs ext4 is 419 MB (arm64) after cleanup; each preset layers ~150–900 MB on top.
Alpine 3.20 with musl libc is ~7 MB compressed at the Docker level vs Ubuntu's ~78 MB. A cleaned Alpine + Node + Python base would land around 200–250 MB ext4 — roughly half the Ubuntu base size and a faster boot to boot.
This issue tracks adding Alpine as an opt-in second flavor users can select per-preset.
Goal
Users pick rootfs flavor at start time:
```
smolvm codex start # default — currently Ubuntu
smolvm codex start --flavor alpine # explicit Alpine
smolvm codex start --flavor ubuntu # explicit Ubuntu
```
Both flavors share the same kernel — Linux's userspace ABI is stable across libc choices, and we already verified our SmolVM-built kernel boots both.
Architecture sketch
Manifest gains a 4th dimension. Today's key is `(preset, arch, vmm)` → 6 rows per preset. Add `flavor`:
```python
Flavor = Literal["ubuntu", "alpine"]
class PublishedImage(BaseModel):
preset: Preset
arch: Arch
vmm: Vmm
flavor: Flavor # new
kernel_url: str
rootfs_url: str
...
```
12 rows per preset (2 archs × 3 vmms × 2 flavors). Kernel is shared across flavors so artifact count on the release page stays modest.
CI workflow in build-published-images.yml gets a flavor matrix dimension:
```yaml
matrix:
arch: [amd64, arm64]
preset: [codex, claude-code, hermes, pi]
flavor: [ubuntu, alpine]
```
Doubles the cells but each Alpine cell is faster (smaller layer copy, faster apk install).
New files under `scripts/ci/`:
- `Dockerfile.base-alpine-rootfs` — `FROM alpine:3.20`, `apk add` equivalents of the Ubuntu base.
- Alpine branch in `build-base-rootfs.sh` selected by `FLAVOR` env var.
- `build-preset.sh` gets per-flavor branches because `apk add` ≠ `apt-get install` and Alpine uses busybox utilities.
CLI flag. Add `--flavor {ubuntu,alpine}` to `smolvm create` and every `smolvm start` command. Default stays Ubuntu until Alpine is fully verified across all presets.
Per-preset risk assessment
Alpine's musl libc breaks some upstream binaries that ship glibc-only prebuilts. Roll out preset-by-preset:
| Preset |
Risk |
Why |
| codex |
low |
pure JS, npm install only |
| claude-code |
low |
pure JS, npm install only |
| pi |
low |
pure JS |
| openclaw |
high |
`@node-llama-cpp` has native bindings shipped as glibc-only prebuilts. Even after we strip CUDA/Vulkan backends, the remaining `linux-x64-cpu` / `linux-arm64-cpu` may not load on musl. |
| hermes |
high |
`[all]` extras pull ML packages. Many ship `manylinux2014` wheels (glibc) but not `musllinux` variants. uv would fall through to source builds — slow, sometimes broken. |
Proposed milestones
- Ship Alpine for codex + claude-code + pi (low risk, ~1-2 days). Three pure-JS presets that almost certainly work.
- Spike hermes on Alpine with `[all]` swapped for a curated minimal extras set if needed. Document any wheels that fail.
- Spike openclaw on Alpine — separate issue if @node-llama-cpp turns out to need a fork or upstream fix.
Each milestone is mergeable independently. Don't gate the whole rollout on the riskiest preset.
Acceptance criteria
- `smolvm codex start --flavor alpine` boots, SSH works, `codex --version` runs.
- Same for `claude-code` and `pi`.
- Default stays Ubuntu unless explicitly selected.
- Alpine rootfs published under the same draft release tag as Ubuntu (one tag, multiple artifacts: `--ubuntu-rootfs.ext4.zst`, `--alpine-rootfs.ext4.zst`).
What's NOT in scope
- Alpine for openclaw or hermes — separate issues if/when we tackle them.
- Distroless / Wolfi / other non-Alpine non-Ubuntu flavors — premature.
- Switching default to Alpine — measure stability under real usage first.
- A flavor-aware `smolvm prune` that distinguishes orphan Alpine vs Ubuntu caches — straightforward extension once flavors land.
Related
Context
Today the layered image build (#263) ships one rootfs flavor per preset: Ubuntu 24.04 with Node 22, Python 3, uv, git, openssh. Base rootfs ext4 is 419 MB (arm64) after cleanup; each preset layers ~150–900 MB on top.
Alpine 3.20 with musl libc is ~7 MB compressed at the Docker level vs Ubuntu's ~78 MB. A cleaned Alpine + Node + Python base would land around 200–250 MB ext4 — roughly half the Ubuntu base size and a faster boot to boot.
This issue tracks adding Alpine as an opt-in second flavor users can select per-preset.
Goal
Users pick rootfs flavor at start time:
```
smolvm codex start # default — currently Ubuntu
smolvm codex start --flavor alpine # explicit Alpine
smolvm codex start --flavor ubuntu # explicit Ubuntu
```
Both flavors share the same kernel — Linux's userspace ABI is stable across libc choices, and we already verified our SmolVM-built kernel boots both.
Architecture sketch
Manifest gains a 4th dimension. Today's key is `(preset, arch, vmm)` → 6 rows per preset. Add `flavor`:
```python
Flavor = Literal["ubuntu", "alpine"]
class PublishedImage(BaseModel):
preset: Preset
arch: Arch
vmm: Vmm
flavor: Flavor # new
kernel_url: str
rootfs_url: str
...
```
12 rows per preset (2 archs × 3 vmms × 2 flavors). Kernel is shared across flavors so artifact count on the release page stays modest.
CI workflow in build-published-images.yml gets a flavor matrix dimension:
```yaml
matrix:
arch: [amd64, arm64]
preset: [codex, claude-code, hermes, pi]
flavor: [ubuntu, alpine]
```
Doubles the cells but each Alpine cell is faster (smaller layer copy, faster apk install).
New files under `scripts/ci/`:
CLI flag. Add `--flavor {ubuntu,alpine}` to `smolvm create` and every `smolvm start` command. Default stays Ubuntu until Alpine is fully verified across all presets.
Per-preset risk assessment
Alpine's musl libc breaks some upstream binaries that ship glibc-only prebuilts. Roll out preset-by-preset:
Proposed milestones
Each milestone is mergeable independently. Don't gate the whole rollout on the riskiest preset.
Acceptance criteria
What's NOT in scope
Related