Skip to content

Commit 2497bc0

Browse files
committed
docs: add Go deploy guide and runnable example for Datum compute
1 parent 5852ed0 commit 2497bc0

8 files changed

Lines changed: 524 additions & 0 deletions

File tree

docs/guides/deploy-a-go-app.md

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# Deploy a Go Web Service on Datum Compute
2+
3+
> Last verified: 2026-06-02 against the `hello-go` example and the live `kraft` / `datumctl compute` CLIs.
4+
> The complete, ready-to-deploy example for this guide lives in [`examples/hello-go/`](../../examples/hello-go/).
5+
6+
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:
7+
8+
- A static-PIE Go binary packaged as a Unikraft unikernel image
9+
- The image published to the Unikraft Cloud metro registry
10+
- A running workload deployed with `datumctl compute deploy`
11+
- A verified HTTP response from your instance
12+
13+
**What you need before starting:**
14+
15+
- `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.
16+
- `datumctl` installed with the compute plugin, authenticated to your Datum Cloud project.
17+
- Docker (with BuildKit) running locally.
18+
- Go 1.22+ (for local development only — the build happens inside Docker).
19+
20+
---
21+
22+
## 1. Write the application
23+
24+
Create a project directory and add two files.
25+
26+
**`main.go`**
27+
28+
```go
29+
package main
30+
31+
import (
32+
"fmt"
33+
"net/http"
34+
"os"
35+
)
36+
37+
func main() {
38+
port := os.Getenv("PORT")
39+
if port == "" {
40+
port = "8080"
41+
}
42+
43+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
44+
fmt.Fprintln(w, "Hello from Datum (Go)")
45+
})
46+
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
47+
fmt.Fprintln(w, "ok")
48+
})
49+
50+
fmt.Printf("listening on :%s\n", port)
51+
if err := http.ListenAndServe(":"+port, nil); err != nil {
52+
fmt.Fprintln(os.Stderr, err)
53+
os.Exit(1)
54+
}
55+
}
56+
```
57+
58+
**`go.mod`**
59+
60+
```
61+
module github.com/your-org/hello-go
62+
63+
go 1.22
64+
```
65+
66+
Replace `your-org` with your actual module path. The service has no external dependencies.
67+
68+
---
69+
70+
## 2. Build and publish the unikernel image with `kraft`
71+
72+
### Why the binary must be a static PIE
73+
74+
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:
75+
76+
```
77+
[appelfloader] ELF executable is not position-independent! ... Exec format error (-8)
78+
```
79+
80+
This is the part to get right for Go, because **stock `go build` cannot produce a static PIE for a pure-Go binary**:
81+
82+
- A plain `CGO_ENABLED=0` build is statically linked but **`ET_EXEC` (non-PIE)** — the elfloader rejects it.
83+
- 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.
84+
85+
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.
86+
87+
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`.
88+
89+
### Write the Dockerfile
90+
91+
```dockerfile
92+
# base:latest is an app-elfloader: it requires a static-PIE (ET_DYN, no INTERP).
93+
# Plain `CGO_ENABLED=0 go build` is ET_EXEC (non-PIE), and `-buildmode=pie` alone
94+
# emits an INTERP segment -- both are rejected at boot. Linking statically against
95+
# musl yields a static PIE with no interpreter.
96+
#
97+
# --platform=$BUILDPLATFORM keeps the Go toolchain native to the builder and
98+
# cross-compiles to amd64, avoiding a qemu-emulated amd64 assembler segfault when
99+
# building on an arm64 host.
100+
FROM --platform=$BUILDPLATFORM golang:1.24 AS build
101+
RUN apt-get update && apt-get install -y --no-install-recommends \
102+
binutils file curl xz-utils ca-certificates \
103+
&& rm -rf /var/lib/apt/lists/*
104+
RUN curl -fsSL https://musl.cc/x86_64-linux-musl-cross.tgz -o /tmp/musl.tgz \
105+
&& tar -xzf /tmp/musl.tgz -C /opt && rm /tmp/musl.tgz
106+
ENV PATH="/opt/x86_64-linux-musl-cross/bin:${PATH}"
107+
WORKDIR /src
108+
COPY go.mod main.go ./
109+
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc \
110+
go build -buildmode=pie \
111+
-ldflags='-s -w -linkmode=external -extldflags "-static-pie"' \
112+
-o /server ./main.go
113+
114+
# Static-PIE self-check: fail the build unless /server is ET_DYN, statically
115+
# linked, and has no INTERP segment.
116+
RUN readelf -h /server | grep -q 'Type:[[:space:]]*DYN' \
117+
|| (echo "FAIL: binary is not ET_DYN (not PIE)"; exit 1)
118+
RUN if readelf -l /server | grep -qi 'INTERP'; then \
119+
echo "FAIL: binary has a program interpreter (INTERP segment)"; exit 1; \
120+
fi
121+
RUN if file /server | grep -q 'dynamically linked'; then \
122+
echo "FAIL: binary is dynamically linked"; exit 1; \
123+
fi
124+
125+
# Stage 2: a minimal rootfs containing only the binary.
126+
FROM scratch
127+
COPY --from=build /server /server
128+
ENTRYPOINT ["/server"]
129+
```
130+
131+
### Write the Kraftfile
132+
133+
```yaml
134+
spec: v0.6
135+
136+
runtime: base:latest
137+
138+
rootfs: ./Dockerfile
139+
140+
cmd: ["/server"]
141+
```
142+
143+
`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.
144+
145+
### Start a BuildKit daemon
146+
147+
`kraft` uses BuildKit to build the rootfs. Start one if you don't already have one running:
148+
149+
```sh
150+
docker run -d --name buildkit --privileged moby/buildkit:latest
151+
```
152+
153+
### Build and publish with `kraft cloud deploy --no-start`
154+
155+
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/<name>`. The `-M` flag sets the memory allocation in MiB and is required.
156+
157+
```sh
158+
export KRAFTKIT_NO_CHECK_UPDATES=true
159+
160+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
161+
--buildkit-host docker-container://buildkit \
162+
deploy --no-start -M 512 --name hello-go \
163+
--runtime base:latest --rootfs ./Dockerfile .
164+
```
165+
166+
After this command completes, your image is available at `index.unikraft.io/datum/hello-go:latest`, ready for Datum compute to deploy.
167+
168+
---
169+
170+
## 3. Deploy on Datum compute
171+
172+
You have two options: a manifest file (recommended for repeatability) or flags.
173+
174+
### Option A — manifest file (recommended)
175+
176+
Create `workload.yaml`:
177+
178+
```yaml
179+
apiVersion: compute.datumapis.com/v1alpha
180+
kind: Workload
181+
metadata:
182+
name: hello-go
183+
labels:
184+
app: hello-go
185+
spec:
186+
template:
187+
metadata:
188+
labels:
189+
app: hello-go
190+
spec:
191+
runtime:
192+
resources:
193+
instanceType: datumcloud/d1-standard-2
194+
sandbox:
195+
containers:
196+
- name: app
197+
image: index.unikraft.io/datum/hello-go:latest
198+
ports:
199+
- name: http
200+
port: 8080
201+
protocol: TCP
202+
networkInterfaces:
203+
- network:
204+
name: default
205+
placements:
206+
- name: default
207+
cityCodes:
208+
- DFW
209+
scaleSettings:
210+
minReplicas: 1
211+
instanceManagementPolicy: OrderedReady
212+
```
213+
214+
Deploy it:
215+
216+
```sh
217+
datumctl compute deploy -f workload.yaml -y
218+
```
219+
220+
### Option B — flags
221+
222+
```sh
223+
datumctl compute deploy hello-go \
224+
--image=index.unikraft.io/datum/hello-go:latest \
225+
--city=DFW \
226+
--port=8080 \
227+
--min=1
228+
```
229+
230+
Both forms create (or update) the workload. The `--city` flag accepts one or more city codes; `DFW` targets the US Central region.
231+
232+
---
233+
234+
## 4. Verify the instance is running
235+
236+
List instances and watch for the status to reach `Running`:
237+
238+
```sh
239+
datumctl compute instances --workload=hello-go
240+
```
241+
242+
A healthy instance shows `Ready: true` and `Running`. The `EXTERNAL IP` column is populated once the instance is live.
243+
244+
For a detailed view of a single instance, including conditions and any failure reason:
245+
246+
```sh
247+
datumctl compute instances describe <instance-name>
248+
```
249+
250+
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:
251+
252+
```sh
253+
# Get the external IP or hostname from the instance list, then:
254+
curl https://<EXTERNAL-IP>/
255+
# -> Hello from Datum (Go)
256+
257+
curl https://<EXTERNAL-IP>/healthz
258+
# -> ok
259+
```
260+
261+
Use `-k` if the TLS certificate is self-signed in your metro:
262+
263+
```sh
264+
curl -k https://<EXTERNAL-IP>/
265+
```
266+
267+
---
268+
269+
## 5. Update the workload
270+
271+
To deploy a new version, rebuild and push the image (repeating step 2), then redeploy. Using the manifest:
272+
273+
```sh
274+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
275+
--buildkit-host docker-container://buildkit \
276+
deploy --no-start -M 512 --name hello-go \
277+
--runtime base:latest --rootfs ./Dockerfile .
278+
279+
datumctl compute deploy -f workload.yaml -y
280+
```
281+
282+
Or with flags:
283+
284+
```sh
285+
datumctl compute deploy hello-go \
286+
--image=index.unikraft.io/datum/hello-go:latest \
287+
--city=DFW \
288+
--port=8080
289+
```
290+
291+
Watch the rollout progress:
292+
293+
```sh
294+
datumctl compute rollout hello-go
295+
```
296+
297+
---
298+
299+
## 6. Clean up
300+
301+
```sh
302+
# Delete the workload and all its instances.
303+
datumctl compute destroy hello-go -y
304+
305+
# Stop the local BuildKit daemon.
306+
docker rm -f buildkit
307+
```
308+
309+
---
310+
311+
## Troubleshooting
312+
313+
### The image fails to boot: "ELF not position-independent" or page fault
314+
315+
```
316+
[appelfloader] probe: ELF executable is not position-independent! ... Exec format error (-8)
317+
```
318+
319+
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:
320+
321+
- 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.
322+
- The `FROM scratch` stage is present.
323+
- 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.
324+
325+
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.
326+
327+
### Instance shows `Ready` but the endpoint doesn't respond
328+
329+
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:
330+
331+
```sh
332+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
333+
instance logs <ukc-instance-name>
334+
```
335+
336+
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 `<ukc-instance-name>` appears in the instance's details from `datumctl compute instances describe <instance-name>`.
337+
338+
### Image pull failures on the instance
339+
340+
`datumctl compute instances describe <instance-name>` reports a condition with reason `ImagePullFailed` or similar when the platform cannot reach the image. Confirm:
341+
342+
- The image was pushed to `index.unikraft.io/datum/<name>` (the metro registry), not to an external container registry like GHCR or Docker Hub. The platform pulls from the UKC metro registry.
343+
- The `kraft cloud deploy` command completed without errors and printed the image reference.
344+
- The image name in `workload.yaml` matches exactly what `kraft cloud deploy` reported, including the `latest` tag.
345+
346+
### Instance is stuck and not progressing
347+
348+
```sh
349+
datumctl compute instances describe <instance-name>
350+
```
351+
352+
Look at the conditions in the output. Common states:
353+
354+
- `QuotaGranted: False` — compute quota has not been provisioned for the project. Contact your platform operator.
355+
- `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.
356+
- `Ready: False, reason: SchedulingGatesPresent` — a scheduling prerequisite (such as a network) has not been satisfied. Confirm your project has a `default` Network resource provisioned.

examples/hello-go/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Build artifacts produced locally; not committed.
2+
/server
3+
.unikraft/

0 commit comments

Comments
 (0)