From 2497bc06e9600cd9bbfbfe12780f5ec9917509eb Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 2 Jun 2026 15:35:55 -0500 Subject: [PATCH] docs: add Go deploy guide and runnable example for Datum compute --- docs/guides/deploy-a-go-app.md | 356 ++++++++++++++++++++++++++++++++ examples/hello-go/.gitignore | 3 + examples/hello-go/Dockerfile | 56 +++++ examples/hello-go/Kraftfile | 7 + examples/hello-go/README.md | 39 ++++ examples/hello-go/go.mod | 3 + examples/hello-go/main.go | 27 +++ examples/hello-go/workload.yaml | 33 +++ 8 files changed, 524 insertions(+) create mode 100644 docs/guides/deploy-a-go-app.md create mode 100644 examples/hello-go/.gitignore create mode 100644 examples/hello-go/Dockerfile create mode 100644 examples/hello-go/Kraftfile create mode 100644 examples/hello-go/README.md create mode 100644 examples/hello-go/go.mod create mode 100644 examples/hello-go/main.go create mode 100644 examples/hello-go/workload.yaml diff --git a/docs/guides/deploy-a-go-app.md b/docs/guides/deploy-a-go-app.md new file mode 100644 index 00000000..75e15a32 --- /dev/null +++ b/docs/guides/deploy-a-go-app.md @@ -0,0 +1,356 @@ +# Deploy a Go Web Service on Datum Compute + +> Last verified: 2026-06-02 against the `hello-go` example and the live `kraft` / `datumctl compute` CLIs. +> The complete, ready-to-deploy example for this guide lives in [`examples/hello-go/`](../../examples/hello-go/). + +This guide walks you through taking a Go HTTP service from source code to a live, reachable instance on Datum compute. By the end you will have: + +- A static-PIE Go binary packaged as a Unikraft unikernel image +- The image published to the Unikraft Cloud metro registry +- A running workload deployed with `datumctl compute deploy` +- A verified HTTP response from your instance + +**What you need before starting:** + +- `kraft` (KraftKit) installed and authenticated to your Unikraft Cloud metro. The metro URL and token are supplied to `kraft cloud` commands; this guide assumes they are available as `$UKC_METRO` and `$UKC_TOKEN` in your shell. +- `datumctl` installed with the compute plugin, authenticated to your Datum Cloud project. +- Docker (with BuildKit) running locally. +- Go 1.22+ (for local development only — the build happens inside Docker). + +--- + +## 1. Write the application + +Create a project directory and add two files. + +**`main.go`** + +```go +package main + +import ( + "fmt" + "net/http" + "os" +) + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello from Datum (Go)") + }) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "ok") + }) + + fmt.Printf("listening on :%s\n", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +``` + +**`go.mod`** + +``` +module github.com/your-org/hello-go + +go 1.22 +``` + +Replace `your-org` with your actual module path. The service has no external dependencies. + +--- + +## 2. Build and publish the unikernel image with `kraft` + +### Why the binary must be a static PIE + +Datum's Unikraft runtime uses an app-elfloader that loads your binary directly as the unikernel entrypoint. It requires a **position-independent executable (static PIE)**: an `ET_DYN` binary that is statically linked and has no program interpreter. A binary that doesn't meet this is rejected at boot: + +``` +[appelfloader] ELF executable is not position-independent! ... Exec format error (-8) +``` + +This is the part to get right for Go, because **stock `go build` cannot produce a static PIE for a pure-Go binary**: + +- A plain `CGO_ENABLED=0` build is statically linked but **`ET_EXEC` (non-PIE)** — the elfloader rejects it. +- Adding `-buildmode=pie` makes it position-independent but emits an **`INTERP` segment** requesting `/lib64/ld-linux-x86-64.so.2` — and `base:latest` has no dynamic loader, so that's rejected too. + +The working recipe (the same approach the Rust example uses) is to link statically against **musl** via CGO, which yields a static PIE with no interpreter. The Dockerfile below installs a musl cross-toolchain and builds with `-buildmode=pie -extldflags "-static-pie"`, then runs a self-check that fails the build unless the result is `ET_DYN` with no `INTERP` — so a wrong-shaped binary can never be published. + +A plain `docker build` OCI image will NOT boot on the runtime regardless. The image must be in the Unikraft Cloud format produced by `kraft`. + +### Write the Dockerfile + +```dockerfile +# base:latest is an app-elfloader: it requires a static-PIE (ET_DYN, no INTERP). +# Plain `CGO_ENABLED=0 go build` is ET_EXEC (non-PIE), and `-buildmode=pie` alone +# emits an INTERP segment -- both are rejected at boot. Linking statically against +# musl yields a static PIE with no interpreter. +# +# --platform=$BUILDPLATFORM keeps the Go toolchain native to the builder and +# cross-compiles to amd64, avoiding a qemu-emulated amd64 assembler segfault when +# building on an arm64 host. +FROM --platform=$BUILDPLATFORM golang:1.24 AS build +RUN apt-get update && apt-get install -y --no-install-recommends \ + binutils file curl xz-utils ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN curl -fsSL https://musl.cc/x86_64-linux-musl-cross.tgz -o /tmp/musl.tgz \ + && tar -xzf /tmp/musl.tgz -C /opt && rm /tmp/musl.tgz +ENV PATH="/opt/x86_64-linux-musl-cross/bin:${PATH}" +WORKDIR /src +COPY go.mod main.go ./ +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc \ + go build -buildmode=pie \ + -ldflags='-s -w -linkmode=external -extldflags "-static-pie"' \ + -o /server ./main.go + +# Static-PIE self-check: fail the build unless /server is ET_DYN, statically +# linked, and has no INTERP segment. +RUN readelf -h /server | grep -q 'Type:[[:space:]]*DYN' \ + || (echo "FAIL: binary is not ET_DYN (not PIE)"; exit 1) +RUN if readelf -l /server | grep -qi 'INTERP'; then \ + echo "FAIL: binary has a program interpreter (INTERP segment)"; exit 1; \ + fi +RUN if file /server | grep -q 'dynamically linked'; then \ + echo "FAIL: binary is dynamically linked"; exit 1; \ + fi + +# Stage 2: a minimal rootfs containing only the binary. +FROM scratch +COPY --from=build /server /server +ENTRYPOINT ["/server"] +``` + +### Write the Kraftfile + +```yaml +spec: v0.6 + +runtime: base:latest + +rootfs: ./Dockerfile + +cmd: ["/server"] +``` + +`runtime: base:latest` is the Unikraft Cloud app-elfloader runtime. `rootfs: ./Dockerfile` tells `kraft` to build the rootfs from your Dockerfile rather than expecting a pre-built image. + +### Start a BuildKit daemon + +`kraft` uses BuildKit to build the rootfs. Start one if you don't already have one running: + +```sh +docker run -d --name buildkit --privileged moby/buildkit:latest +``` + +### Build and publish with `kraft cloud deploy --no-start` + +Use `kraft` only to build and publish the image — you deploy the running workload with `datumctl compute` in the next step. The `--no-start` (`-S`) flag builds the unikernel package and pushes it to the metro registry **without** starting an instance, so `kraft` never runs your workload. It pushes to `index.unikraft.io/datum/`. The `-M` flag sets the memory allocation in MiB and is required. + +```sh +export KRAFTKIT_NO_CHECK_UPDATES=true + +kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \ + --buildkit-host docker-container://buildkit \ + deploy --no-start -M 512 --name hello-go \ + --runtime base:latest --rootfs ./Dockerfile . +``` + +After this command completes, your image is available at `index.unikraft.io/datum/hello-go:latest`, ready for Datum compute to deploy. + +--- + +## 3. Deploy on Datum compute + +You have two options: a manifest file (recommended for repeatability) or flags. + +### Option A — manifest file (recommended) + +Create `workload.yaml`: + +```yaml +apiVersion: compute.datumapis.com/v1alpha +kind: Workload +metadata: + name: hello-go + labels: + app: hello-go +spec: + template: + metadata: + labels: + app: hello-go + spec: + runtime: + resources: + instanceType: datumcloud/d1-standard-2 + sandbox: + containers: + - name: app + image: index.unikraft.io/datum/hello-go:latest + ports: + - name: http + port: 8080 + protocol: TCP + networkInterfaces: + - network: + name: default + placements: + - name: default + cityCodes: + - DFW + scaleSettings: + minReplicas: 1 + instanceManagementPolicy: OrderedReady +``` + +Deploy it: + +```sh +datumctl compute deploy -f workload.yaml -y +``` + +### Option B — flags + +```sh +datumctl compute deploy hello-go \ + --image=index.unikraft.io/datum/hello-go:latest \ + --city=DFW \ + --port=8080 \ + --min=1 +``` + +Both forms create (or update) the workload. The `--city` flag accepts one or more city codes; `DFW` targets the US Central region. + +--- + +## 4. Verify the instance is running + +List instances and watch for the status to reach `Running`: + +```sh +datumctl compute instances --workload=hello-go +``` + +A healthy instance shows `Ready: true` and `Running`. The `EXTERNAL IP` column is populated once the instance is live. + +For a detailed view of a single instance, including conditions and any failure reason: + +```sh +datumctl compute instances describe +``` + +Once the instance is `Running`, curl the external endpoint. UKC fronts the service with TLS on port 443 and redirects plain HTTP on port 80: + +```sh +# Get the external IP or hostname from the instance list, then: +curl https:/// +# -> Hello from Datum (Go) + +curl https:///healthz +# -> ok +``` + +Use `-k` if the TLS certificate is self-signed in your metro: + +```sh +curl -k https:/// +``` + +--- + +## 5. Update the workload + +To deploy a new version, rebuild and push the image (repeating step 2), then redeploy. Using the manifest: + +```sh +kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \ + --buildkit-host docker-container://buildkit \ + deploy --no-start -M 512 --name hello-go \ + --runtime base:latest --rootfs ./Dockerfile . + +datumctl compute deploy -f workload.yaml -y +``` + +Or with flags: + +```sh +datumctl compute deploy hello-go \ + --image=index.unikraft.io/datum/hello-go:latest \ + --city=DFW \ + --port=8080 +``` + +Watch the rollout progress: + +```sh +datumctl compute rollout hello-go +``` + +--- + +## 6. Clean up + +```sh +# Delete the workload and all its instances. +datumctl compute destroy hello-go -y + +# Stop the local BuildKit daemon. +docker rm -f buildkit +``` + +--- + +## Troubleshooting + +### The image fails to boot: "ELF not position-independent" or page fault + +``` +[appelfloader] probe: ELF executable is not position-independent! ... Exec format error (-8) +``` + +This means the binary is not a static PIE. The most common cause is building with `CGO_ENABLED=0` (or `-buildmode=pie` alone): both produce a binary the elfloader rejects — `CGO_ENABLED=0` is `ET_EXEC` (non-PIE), and `-buildmode=pie` adds an `INTERP` segment. Use the musl static-PIE recipe in the Dockerfile above. Check: + +- The build linked statically against musl (`CGO_ENABLED=1 CC=x86_64-linux-musl-gcc ... -buildmode=pie -extldflags "-static-pie"`). The Dockerfile's self-check confirms the result is `ET_DYN` with no `INTERP` before it can be published — if your build printed the self-check lines, the shape is correct. +- The `FROM scratch` stage is present. +- The image was built with `kraft cloud deploy`, not plain `docker build`. A plain OCI image pushed to a container registry will not boot on the Unikraft runtime regardless of how the binary is built. + +Rebuild from the Dockerfile in this guide, re-publish (step 2), and redeploy. The boot error appears on the unikernel console — see "Instance shows `Ready` but the endpoint doesn't respond" below to read it. + +### Instance shows `Ready` but the endpoint doesn't respond + +If an instance reports `Ready` but a `curl` to its endpoint hangs or fails, the unikernel may not have booted cleanly. The unikernel console is the source of truth — read it directly: + +```sh +kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \ + instance logs +``` + +A healthy boot prints your `listening on :8080` line. A boot error such as `appelfloader ... not position-independent` means the image must be rebuilt as a fully static binary (see the first troubleshooting entry). The `` appears in the instance's details from `datumctl compute instances describe `. + +### Image pull failures on the instance + +`datumctl compute instances describe ` reports a condition with reason `ImagePullFailed` or similar when the platform cannot reach the image. Confirm: + +- The image was pushed to `index.unikraft.io/datum/` (the metro registry), not to an external container registry like GHCR or Docker Hub. The platform pulls from the UKC metro registry. +- The `kraft cloud deploy` command completed without errors and printed the image reference. +- The image name in `workload.yaml` matches exactly what `kraft cloud deploy` reported, including the `latest` tag. + +### Instance is stuck and not progressing + +```sh +datumctl compute instances describe +``` + +Look at the conditions in the output. Common states: + +- `QuotaGranted: False` — compute quota has not been provisioned for the project. Contact your platform operator. +- `Programmed: False` — the instance has not been scheduled to a node yet. This is normal for a few seconds after deploy; if it persists, check that the city code in your workload matches an available location. +- `Ready: False, reason: SchedulingGatesPresent` — a scheduling prerequisite (such as a network) has not been satisfied. Confirm your project has a `default` Network resource provisioned. diff --git a/examples/hello-go/.gitignore b/examples/hello-go/.gitignore new file mode 100644 index 00000000..337adf2e --- /dev/null +++ b/examples/hello-go/.gitignore @@ -0,0 +1,3 @@ +# Build artifacts produced locally; not committed. +/server +.unikraft/ diff --git a/examples/hello-go/Dockerfile b/examples/hello-go/Dockerfile new file mode 100644 index 00000000..ba532c3e --- /dev/null +++ b/examples/hello-go/Dockerfile @@ -0,0 +1,56 @@ +# Multi-stage build for the static-PIE Go runtime. +# +# The Unikraft Cloud base:latest runtime is an app-elfloader that (a) REQUIRES a +# position-independent executable and (b) has NO dynamic loader, so the app ELF +# must be a static-PIE with no program interpreter. Stock `go build` cannot +# produce that for a pure-Go binary: -buildmode=pie always emits an INTERP +# segment requesting /lib64/ld-linux-x86-64.so.2, and a plain static build is +# non-PIE (ET_EXEC) -- both are rejected at boot with +# "ELF executable is not position-independent! ... Exec format error (-8)". +# +# The fix mirrors the Rust path: link statically against musl, which yields a +# static-PIE (ET_DYN, statically linked, no INTERP). We cross-link with the musl +# x86_64 toolchain via CGO so `go build` emits a self-contained app ELF. +# +# --platform=$BUILDPLATFORM keeps the Go toolchain native to the builder and +# lets Go cross-compile to amd64, avoiding the qemu-emulated amd64 toolchain that +# segfaults the assembler. The musl cross-compiler is installed for the external +# static-PIE link step. +# +# A self-check FAILS the build unless /server is a static-PIE with no INTERP, so +# a bad shape can never be published. +FROM --platform=$BUILDPLATFORM golang:1.24 AS build +RUN apt-get update && apt-get install -y --no-install-recommends \ + binutils file curl xz-utils ca-certificates \ + && rm -rf /var/lib/apt/lists/* +# musl.cc prebuilt cross toolchain: x86_64 musl gcc that runs on the (arm64) +# builder and emits x86_64 ELFs, so no qemu emulation of the compiler. +RUN curl -fsSL https://musl.cc/x86_64-linux-musl-cross.tgz -o /tmp/musl.tgz \ + && tar -xzf /tmp/musl.tgz -C /opt && rm /tmp/musl.tgz +ENV PATH="/opt/x86_64-linux-musl-cross/bin:${PATH}" +WORKDIR /src +COPY go.mod main.go ./ +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc \ + go build -buildmode=pie \ + -ldflags='-s -w -linkmode=external -extldflags "-static-pie"' \ + -o /server ./main.go + +# Static-PIE self-check: fail the build unless the binary is ET_DYN (PIE) AND +# statically linked AND has no INTERP segment. +RUN echo "=== readelf -h /server ===" && readelf -h /server && \ + echo "=== file /server ===" && file /server && \ + echo "=== readelf -l (INTERP check) ===" && (readelf -l /server | grep -i interp || echo "no INTERP segment") +RUN readelf -h /server | grep -q 'Type:[[:space:]]*DYN' \ + || (echo "FAIL: binary is not ET_DYN (not PIE)"; exit 1) +RUN if readelf -l /server | grep -qi 'INTERP'; then \ + echo "FAIL: binary has a program interpreter (INTERP segment)"; exit 1; \ + fi +RUN if file /server | grep -q 'dynamically linked'; then \ + echo "FAIL: binary is dynamically linked"; exit 1; \ + fi +RUN echo "OK: /server is a static-PIE ELF (ET_DYN, no INTERP)" + +# Stage 2: a minimal rootfs containing only the binary. +FROM scratch +COPY --from=build /server /server +ENTRYPOINT ["/server"] diff --git a/examples/hello-go/Kraftfile b/examples/hello-go/Kraftfile new file mode 100644 index 00000000..abe5d412 --- /dev/null +++ b/examples/hello-go/Kraftfile @@ -0,0 +1,7 @@ +spec: v0.6 + +runtime: base:latest + +rootfs: ./Dockerfile + +cmd: ["/server"] diff --git a/examples/hello-go/README.md b/examples/hello-go/README.md new file mode 100644 index 00000000..b75a5a89 --- /dev/null +++ b/examples/hello-go/README.md @@ -0,0 +1,39 @@ +# hello-go + +A minimal Go HTTP service packaged as a Unikraft unikernel and deployed on Datum +compute. It responds `Hello from Datum (Go)` on `/` and `ok` on `/healthz`, listening +on `$PORT` (default `8080`). + +This is the runnable companion to the step-by-step guide: +[Deploy a Go Web Service on Datum Compute](../../docs/guides/deploy-a-go-app.md). + +## Files + +- `main.go` — the service (standard library only, no dependencies). +- `go.mod` — module definition. +- `Dockerfile` — multi-stage build producing a static-PIE binary (linked against + musl) packaged `FROM scratch`, so the app is the unikernel entrypoint. A static + PIE is required: the `base:latest` elfloader rejects a plain `CGO_ENABLED=0` + (`ET_EXEC`, non-PIE) binary. Includes a build-time self-check. +- `Kraftfile` — packages the rootfs on the `base:latest` 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 -M 512 --name hello-go \ + --runtime base:latest --rootfs ./Dockerfile . + +# 2. Deploy on Datum compute. +datumctl compute deploy -f workload.yaml -y + +# 3. Verify. +datumctl compute instances --workload=hello-go +curl https:/// +``` + +See the [guide](../../docs/guides/deploy-a-go-app.md) for prerequisites, the +explanation of why the binary must be static, and troubleshooting. diff --git a/examples/hello-go/go.mod b/examples/hello-go/go.mod new file mode 100644 index 00000000..7dc4860c --- /dev/null +++ b/examples/hello-go/go.mod @@ -0,0 +1,3 @@ +module github.com/your-org/hello-go + +go 1.22 diff --git a/examples/hello-go/main.go b/examples/hello-go/main.go new file mode 100644 index 00000000..13c94d09 --- /dev/null +++ b/examples/hello-go/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "net/http" + "os" +) + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello from Datum (Go)") + }) + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "ok") + }) + + fmt.Printf("listening on :%s\n", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/examples/hello-go/workload.yaml b/examples/hello-go/workload.yaml new file mode 100644 index 00000000..317f6774 --- /dev/null +++ b/examples/hello-go/workload.yaml @@ -0,0 +1,33 @@ +apiVersion: compute.datumapis.com/v1alpha +kind: Workload +metadata: + name: hello-go + labels: + app: hello-go +spec: + template: + metadata: + labels: + app: hello-go + spec: + runtime: + resources: + instanceType: datumcloud/d1-standard-2 + sandbox: + containers: + - name: app + image: index.unikraft.io/datum/hello-go:latest + ports: + - name: http + port: 8080 + protocol: TCP + networkInterfaces: + - network: + name: default + placements: + - name: default + cityCodes: + - DFW + scaleSettings: + minReplicas: 1 + instanceManagementPolicy: OrderedReady