Bootstrap
The initialization sequence from Unix() builder to running system.
Overview
The bootstrap has three distinct phases:
- Build —
Unix().use(...).build()produces a frozenUnixImage - Runtime —
nodeRuntime(),browserRuntime(), ortestRuntime()provides platform capabilities - Boot —
runtime.boot(image, opts)assembles the namespace, creates the kernel, starts PID 1
Unix() -- configure .use(stdSystem()) .use(myTools()) .build() -- freeze into image
nodeRuntime() -- platform capabilities .boot(image, { tty }) -- assemble namespace, create kernel, start PID 1Build Phase
function Unix(): UnixBuilder
interface UnixBuilder { use(ext: Extension): UnixBuilder // primary composition mount(path, server): UnixBuilder // sugar over use() bin(name, fn): UnixBuilder env(key, value): UnixBuilder file(path, content): UnixBuilder service(def): UnixBuilder run(command): UnixBuilder // execute shell command during build catalog(name, factory): UnixBuilder // register offline package build(): Promise<UnixImage> // freeze into image}The builder accumulates Extensions and produces a frozen image. Each method returns a new builder (immutable). .build() merges all Extensions, writes exec nodes to a MemoryFS, executes .run() steps (each producing a frozen overlay layer), and returns the image.
Decision: builder is immutable. Each
.use()returns a new builder. Forking is safe — base configs can branch into specialized variants without interference.
Image
An image is a frozen, immutable system configuration:
interface UnixImage { createBootContext(): BootContext // data contract for Runtime extend(): UnixBuilder // derive a new builder from this image}The image holds:
- Frozen rootFS — stacked overlay layers from
.build()and.run()steps - Merged config — bins, env, mounts, services, catalog entries
createBootContext() returns a read-only view of the image’s contents for the Runtime to consume. extend() creates a new builder pre-loaded with the image’s layers.
Runtime Phase
The Runtime sits between Image and Instance. It knows what platform it’s on and provides platform-appropriate capabilities. See runtime architecture for full details.
import { nodeRuntime } from '@fishnet/core/node'import { browserRuntime } from '@fishnet/core/browser'import { testRuntime } from '@fishnet/core/test'
// Each returns a Runtime with platform-specific capabilitiesconst rt = nodeRuntime() // node:fs asset loading, Buffer-based ESM evalconst rt = browserRuntime() // fetch-based loading, Blob URL ESM evalconst rt = testRuntime() // minimal caps, throws on asset loadingBoot Phase
runtime.boot(image, opts) assembles the running system:
runtime.boot(image, opts): 1. Read BootContext from image (rootFs, mounts, bins, env, services, catalogs) 2. Create writable upper layer (memoryFS) 3. Create overlay root: overlayFS(upper, rootFs) 4. Mount devFS at /dev 5. Create processTable + KernelLog 6. Mount procFS at /proc (with signal + getNs callbacks) 7. Mount consFS at /dev/cons (if tty provided) via mountConsTerminal helper 8. Mount userFS at /dev/user 9. Mount volumes from BootOpts 10. Mount srvFS at /srv 11. Write catalog metadata to /pkg/.catalog/ 12. Build environment (DEFAULT_ENV + image env) 13. Build catalogInstall callback 14. Create kernel with mount table + callbacks 15. Determine PID 1 (init with services, or shell) 16. Construct and return UnixInstanceDecision: procFS signal callback captures
kernelvia late-binding closure. The kernel is created in step 14 but referenced in step 6’s procFS callback. This is resolved vialet kernelwith late assignment — the callback reads it lazily, only after the kernel exists.
PID 1
If services are declared, PID 1 is makeInit(services) — a generated init that starts services in dependency order, then starts a shell, then supervises.
If no services, PID 1 is the shell directly. Zero overhead for the common case.
When PID 1 exits:
- All child processes receive SIGTERM
- After 5s grace period, survivors receive SIGKILL
instance.wait()resolves with PID 1’s exit codekernelLog.dispose()unblocks any /proc/log readers
BootOpts
Per-instance configuration provided at boot time:
interface BootOpts { tty?: TtyConfig // terminal streams cwd?: string // initial working directory volumes?: Record<string, Fileserver> // direct mounts (bypass overlay)}Complete Example
import { Unix, stdSystem } from '@fishnet/core'import { nodeRuntime } from '@fishnet/core/node'import { nodeStdio } from '@fishnet/core/node'
// Build imageconst image = await Unix() .use(stdSystem()) .env('EDITOR', 'vim') .build()
// Create runtimeconst rt = nodeRuntime()
// Boot with terminalconst io = await nodeStdio({ prompt: '$ ' })const instance = await rt.boot(image, { tty: io.tty })
// Wait for shell to exitconst exitCode = await instance.boot()Lifecycle
Unix().use(...).build() -- freeze into image | v runtime.boot(image, opts) -- assemble namespace, create kernel | v instance.boot() -- start PID 1 (shell or init) | v Running (processes executing) | v instance.shutdown() -- SIGTERM -> 5s -> SIGKILL -> cleanup | v Disposed