Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions docs/guides/deploy-a-node-app.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions examples/hello-node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Build artifacts produced locally; not committed.
/node_modules/
.unikraft/
41 changes: 41 additions & 0 deletions examples/hello-node/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Multi-stage build for the Node.js runtime proof.
#
# Unlike the Go/Rust proofs, Node is NOT a static-PIE binary -- it is a dynamic
# musl ELF that needs the musl loader and a handful of shared libraries at boot.
# So this rootfs ships the `node` interpreter AND every shared library it links
# against, and the unikernel runs on the base-compat:latest runtime (the
# binary-compatibility / dynamic-loader elfloader variant), NOT base:latest.
#
# Stage 1 (node:22-alpine) installs deps and prints `ldd node` so the build log
# records exactly which shared objects the rootfs must carry. The scratch image
# below has no package manager, so EVERY library node links must be copied
# explicitly; a missing .so fails the instance at boot with library-not-found.
#
# The target is x86_64 (base-compat == kraftcloud/x86_64) and the COPYs below
# pull x86_64 musl libs, so the build stage is pinned to linux/amd64. On an arm64
# host (Apple Silicon) this cross-builds to x86_64; without the pin the
# ld-musl-x86_64.so.1 COPYs fail (or a wrong-arch binary boots).
FROM --platform=linux/amd64 node:22-alpine AS build
WORKDIR /usr/src
COPY package*.json ./
RUN npm install
# npm install with zero deps creates no node_modules dir; ensure it exists so the
# COPY below succeeds and adding deps later "just works" with no Dockerfile change.
RUN mkdir -p node_modules
COPY app.js ./
# Record node's dynamic-library requirements in the build log for auditing.
RUN echo "=== ldd /usr/local/bin/node ===" && ldd /usr/local/bin/node || true

FROM scratch
# The node interpreter.
COPY --from=build /usr/local/bin/node /usr/bin/node
# musl dynamic loader + libc (ld-musl is both loader and libc on alpine).
COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
# C++/GCC runtime libraries node links against.
COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
# os-release lets node/libc identify the platform cleanly.
COPY --from=build /etc/os-release /etc/os-release
# Application + (empty for the hello case) dependency tree.
COPY --from=build /usr/src/node_modules /usr/src/node_modules
COPY --from=build /usr/src/app.js /usr/src/server.js
27 changes: 27 additions & 0 deletions examples/hello-node/Kraftfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Kraftfile for the Node.js runtime proof.
#
# Node is a dynamic musl ELF, so it runs on base-compat:latest (the
# binary-compatibility / dynamic-loader elfloader variant), NOT base:latest used
# by the static-PIE Go/Rust proofs. The rootfs (built from the multi-stage
# Dockerfile) carries the node interpreter plus every shared library it links.
#
# The rootfs MUST be packaged as a CPIO initramfs. An erofs initrd does NOT boot
# on base-compat:latest -- it triggers an instant platform assertion ((i0 EXP) at
# 0.00ms, no console logs). Omitting rootfs.format leaves kraft on CPIO; the build
# command passes --rootfs-type cpio explicitly.
#
# Build/push (push-only, do not start):
# kraft cloud --metro <metro>/v1 --token <token> \
# --buildkit-host docker-container://buildkit \
# deploy --no-start --rootfs-type cpio -M 512 --name hello-node \
# --runtime base-compat:latest --rootfs ./Dockerfile .
spec: v0.7

name: hello-node

runtime: base-compat:latest

rootfs:
source: ./Dockerfile

cmd: ["/usr/bin/node", "/usr/src/server.js"]
55 changes: 55 additions & 0 deletions examples/hello-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# hello-node

A minimal Node.js HTTP service packaged as a Unikraft unikernel and deployed on
Datum compute. It responds `Hello from Datum (Node)` on `/` and `ok` on
`/healthz`, listening on `$PORT` (default `8080`).

This is the runnable companion to the step-by-step guide:
[Deploy a Node.js Web Service on Datum Compute](../../docs/guides/deploy-a-node-app.md).

Unlike the [Go](../hello-go/) and [Rust](../hello-rust/) examples — which are static
binaries on the `base:latest` runtime — Node is a dynamic musl ELF. It runs on the
**`base-compat:latest`** runtime (the binary-compatibility / dynamic-loader variant
of the elfloader), and the rootfs ships the `node` interpreter plus every shared
library it links.

## Files

- `app.js` — the service (standard library only, no dependencies).
- `package.json` — package metadata.
- `Dockerfile` — multi-stage build: an `node:22-alpine` stage installs deps and
records `ldd node`, then a `FROM scratch` rootfs copies the interpreter and its
shared libraries (`ld-musl`, `libstdc++`, `libgcc_s`).
- `Kraftfile` — packages the rootfs on the `base-compat:latest` runtime as a CPIO
initramfs (an `erofs` initrd does not boot on this runtime).
- `workload.yaml` — the Datum compute Workload manifest.

## Quick start

```sh
# 1. Build and publish the image (kraft builds + pushes; it does not run it).
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
--buildkit-host docker-container://buildkit \
deploy --no-start --rootfs-type cpio -M 512 --name hello-node \
--runtime base-compat:latest --rootfs ./Dockerfile .

# 2. Deploy on Datum compute.
datumctl compute deploy -f workload.yaml -y

# 3. Verify.
datumctl compute instances --workload=hello-node
curl https://<EXTERNAL-IP>/
```

See the [guide](../../docs/guides/deploy-a-node-app.md) for the general workflow,
prerequisites, and troubleshooting — the build/publish/deploy mechanics are the
same; only the runtime (`base-compat:latest`) and the libraries copied into the
rootfs differ.

## Note on dependencies

`npm install` with zero dependencies creates no `node_modules` directory, so the
Dockerfile runs `mkdir -p node_modules` before the `COPY`. When you add real
dependencies they are installed in the build stage and copied into the rootfs with
no Dockerfile change. Native (node-gyp) addons need their own compiled `.so` files
copied into the scratch rootfs as well — pure-JS dependencies need nothing extra.
22 changes: 22 additions & 0 deletions examples/hello-node/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Minimal pure-Node HTTP service for the Node.js runtime proof on Datum compute.
//
// No dependencies: the "hello" case ships an empty node_modules so the unikernel
// rootfs stays minimal. Listens on $PORT (default 8080) and logs a clear boot
// marker so the unikernel console shows when Node has come up.
const http = require('http');

const port = parseInt(process.env.PORT, 10) || 8080;

const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok\n');
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Datum (Node)\n');
});

server.listen(port, () => {
console.log('listening on :' + port);
});
10 changes: 10 additions & 0 deletions examples/hello-node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "hello-node",
"version": "1.0.0",
"private": true,
"description": "Minimal Node HTTP service for the Datum compute Node.js runtime proof.",
"main": "app.js",
"scripts": {
"start": "node app.js"
}
}
33 changes: 33 additions & 0 deletions examples/hello-node/workload.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: compute.datumapis.com/v1alpha
kind: Workload
metadata:
name: hello-node
labels:
app: hello-node
spec:
template:
metadata:
labels:
app: hello-node
spec:
runtime:
resources:
instanceType: datumcloud/d1-standard-2
sandbox:
containers:
- name: app
image: index.unikraft.io/datum/hello-node:latest
ports:
- name: http
port: 8080
protocol: TCP
networkInterfaces:
- network:
name: default
placements:
- name: default
cityCodes:
- DFW
scaleSettings:
minReplicas: 1
instanceManagementPolicy: OrderedReady
Loading