Skip to content

Commit 7b3fbf6

Browse files
scotwellsclaude
andcommitted
docs: add guide for deploying a PHP service on Datum compute
Adds a self-contained tutorial that takes a developer from a PHP web service in source form to a live, reachable Instance on Datum compute. It covers packaging the app as a Unikraft unikernel (including how to ship only the precise shared-library closure the PHP interpreter needs so the in-RAM rootfs stays small), publishing the image to the metro registry with kraft, deploying the workload with datumctl compute deploy via either a manifest or flags, verifying the HTTP response, updating and cleaning up the workload, and troubleshooting common boot and pull failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5852ed0 commit 7b3fbf6

1 file changed

Lines changed: 339 additions & 0 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# Deploy a PHP Web Service on Datum Compute
2+
3+
> Last verified: 2026-06-02 against the `hello-php` example and the live `kraft` / `datumctl compute` CLIs.
4+
5+
This guide walks you through taking a PHP HTTP service from source code to a live, reachable instance on Datum compute. By the end you will have:
6+
7+
- A PHP application packaged as a Unikraft unikernel image
8+
- The image published to the Unikraft Cloud metro registry
9+
- A running workload deployed with `datumctl compute deploy`
10+
- A verified HTTP response from your instance
11+
12+
**What you need before starting:**
13+
14+
- `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.
15+
- `datumctl` installed with the compute plugin, authenticated to your Datum Cloud project.
16+
- Docker (with BuildKit) running locally.
17+
- PHP (for local development only — the build happens inside Docker).
18+
19+
---
20+
21+
## 1. Write the application
22+
23+
Create a project directory and add one file. This is a router script for PHP's built-in web server — PHP invokes it for every request.
24+
25+
**`server.php`**
26+
27+
```php
28+
<?php
29+
header('Content-Type: text/plain; charset=utf-8');
30+
31+
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
32+
33+
if ($path === '/healthz') {
34+
echo "ok\n";
35+
} else {
36+
echo "Hello from Datum (PHP)\n";
37+
}
38+
```
39+
40+
The service answers `/healthz` with `ok` and everything else with `Hello from Datum (PHP)`, and has no external dependencies. PHP's built-in server binds the address given on its command line (`0.0.0.0:8080`, set in the Kraftfile below) and prints its own boot marker to the console on start.
41+
42+
---
43+
44+
## 2. Build and publish the unikernel image with `kraft`
45+
46+
### Why PHP ships the interpreter and its library closure
47+
48+
Datum's Unikraft runtime uses an app-elfloader that loads your application as the unikernel entrypoint. Compiled languages (Go, Rust) ship a single fully static binary. PHP is different: the `php` CLI is a **dynamically linked** executable — it needs its loader (`/lib64/ld-linux-x86-64.so.2`), a set of glibc shared libraries, and the libraries its bundled extensions link (e.g. `libssl`/`libcrypto`, `libxml2`, `libsqlite3`, `libcurl`). The rootfs ships the `php` binary, its extensions, the loader, and those shared libraries, and the unikernel runs on the same `base:latest` runtime as Go and Rust.
49+
50+
There is one important constraint to get right. **The unikernel extracts its rootfs into an in-RAM filesystem at boot, so the image must stay small.** Copying the entire `/usr/lib/x86_64-linux-gnu` directory overflows that RAM disk and the boot fails:
51+
52+
```
53+
[libukcpio] ...: Failed to load content: No space left on device (28)
54+
[libposix_vfs_fstab] Failed to extract CPIO to /: -3
55+
```
56+
57+
The fix is to ship **only the precise shared-library closure** the `php` binary and its bundled extensions actually need. The Dockerfile below computes that closure at build time with `ldd`.
58+
59+
A plain `docker build` OCI image will NOT boot on the runtime. The image must be in the Unikraft Cloud format produced by `kraft`. The `Kraftfile` and `kraft cloud deploy` command handle this packaging.
60+
61+
### Write the Dockerfile
62+
63+
```dockerfile
64+
FROM php:8.3.14-cli-bookworm AS base
65+
66+
# Stage the exact shared-library closure for the php binary + every bundled
67+
# extension .so. Walking ldd over each extension captures libraries the
68+
# extensions link (openssl, libxml2, libsodium, libargon2, ...) that a
69+
# hand-written list would miss. SONAME symlinks are preserved so the loader
70+
# resolves NEEDED entries.
71+
RUN set -eu; \
72+
extdir="$(php -r 'echo ini_get("extension_dir");')"; \
73+
mkdir -p /rootfs-libs; \
74+
{ \
75+
ldd /usr/local/bin/php; \
76+
for f in "$extdir"/*.so; do [ -e "$f" ] && ldd "$f"; done; \
77+
} 2>/dev/null \
78+
| awk '/=>/ {print $3}' \
79+
| grep -E '^/(usr/)?lib' \
80+
| sort -u > /tmp/sonames.txt; \
81+
while read -r p; do \
82+
[ -n "$p" ] || continue; \
83+
real="$(readlink -f "$p")"; \
84+
cp -a "$real" "/rootfs-libs/$(basename "$real")"; \
85+
if [ "$(basename "$p")" != "$(basename "$real")" ]; then \
86+
ln -sf "$(basename "$real")" "/rootfs-libs/$(basename "$p")"; \
87+
fi; \
88+
done < /tmp/sonames.txt; \
89+
du -sh /rootfs-libs
90+
91+
FROM scratch
92+
93+
# The php CLI binary.
94+
COPY --from=base /usr/local/bin/php /usr/local/bin/php
95+
96+
# The bundled extensions. Preserve the exact extension_dir path so PHP resolves
97+
# them without an explicit override.
98+
COPY --from=base /usr/local/lib/php /usr/local/lib/php
99+
100+
# glibc dynamic loader (the program interpreter named in the php ELF header).
101+
COPY --from=base /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2
102+
103+
# The precise shared-library closure, under both default loader search paths so
104+
# NEEDED SONAMEs resolve without an ld.so.cache (intentionally not copied: it
105+
# references libraries we did not ship; the loader falls back to its default
106+
# trusted search paths, where these libraries live).
107+
COPY --from=base /rootfs-libs/ /lib/x86_64-linux-gnu/
108+
COPY --from=base /rootfs-libs/ /usr/lib/x86_64-linux-gnu/
109+
110+
COPY ./server.php /server.php
111+
```
112+
113+
> **Note:** pin the interpreter patch version (`php:8.3.14-cli-bookworm`). The bundled extensions live under a version-specific `extension_dir` (e.g. `/usr/local/lib/php/extensions/no-debug-non-zts-<api-no>/`); copying the whole `/usr/local/lib/php` tree preserves that path so PHP finds them.
114+
115+
### Write the Kraftfile
116+
117+
```yaml
118+
spec: v0.6
119+
120+
runtime: base:latest
121+
122+
rootfs: ./Dockerfile
123+
124+
cmd: ["/usr/local/bin/php", "-S", "0.0.0.0:8080", "/server.php"]
125+
```
126+
127+
`runtime: base:latest` is the Unikraft Cloud app-elfloader runtime. The `cmd` runs PHP's built-in web server, which binds the literal `0.0.0.0:8080` and serves every request through `/server.php`. (`php -S` does not read `$PORT`, so the listen port is fixed here — keep it aligned with the workload port.)
128+
129+
### Start a BuildKit daemon
130+
131+
`kraft` uses BuildKit to build the rootfs. Start one if you don't already have one running:
132+
133+
```sh
134+
docker run -d --name buildkit --privileged moby/buildkit:latest
135+
```
136+
137+
### Build and publish with `kraft cloud deploy --no-start`
138+
139+
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. It pushes to `index.unikraft.io/datum/<name>`. The `-M` flag sets the memory allocation in MiB and is required — use at least `1024`.
140+
141+
```sh
142+
export KRAFTKIT_NO_CHECK_UPDATES=true
143+
144+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
145+
--buildkit-host docker-container://buildkit \
146+
deploy --no-start -M 1024 --name hello-php \
147+
--runtime base:latest --rootfs ./Dockerfile .
148+
```
149+
150+
After this command completes, your image is available at `index.unikraft.io/datum/hello-php:latest`, ready for Datum compute to deploy.
151+
152+
---
153+
154+
## 3. Deploy on Datum compute
155+
156+
You have two options: a manifest file (recommended for repeatability) or flags.
157+
158+
### Option A — manifest file (recommended)
159+
160+
Create `workload.yaml`:
161+
162+
```yaml
163+
apiVersion: compute.datumapis.com/v1alpha
164+
kind: Workload
165+
metadata:
166+
name: hello-php
167+
labels:
168+
app: hello-php
169+
spec:
170+
template:
171+
metadata:
172+
labels:
173+
app: hello-php
174+
spec:
175+
runtime:
176+
resources:
177+
instanceType: datumcloud/d1-standard-2
178+
sandbox:
179+
containers:
180+
- name: app
181+
image: index.unikraft.io/datum/hello-php:latest
182+
ports:
183+
- name: http
184+
port: 8080
185+
protocol: TCP
186+
networkInterfaces:
187+
- network:
188+
name: default
189+
placements:
190+
- name: default
191+
cityCodes:
192+
- DFW
193+
scaleSettings:
194+
minReplicas: 1
195+
instanceManagementPolicy: OrderedReady
196+
```
197+
198+
Deploy it:
199+
200+
```sh
201+
datumctl compute deploy -f workload.yaml -y
202+
```
203+
204+
### Option B — flags
205+
206+
```sh
207+
datumctl compute deploy hello-php \
208+
--image=index.unikraft.io/datum/hello-php:latest \
209+
--city=DFW \
210+
--port=8080 \
211+
--min=1
212+
```
213+
214+
Both forms create (or update) the workload. The `--city` flag accepts one or more city codes; `DFW` targets the US Central region.
215+
216+
---
217+
218+
## 4. Verify the instance is running
219+
220+
List instances and watch for the status to reach `Running`:
221+
222+
```sh
223+
datumctl compute instances --workload=hello-php
224+
```
225+
226+
A healthy instance shows `Ready: true` and `Running`. The `EXTERNAL IP` column is populated once the instance is live.
227+
228+
For a detailed view of a single instance, including conditions and any failure reason:
229+
230+
```sh
231+
datumctl compute instances describe <instance-name>
232+
```
233+
234+
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:
235+
236+
```sh
237+
# Get the external IP or hostname from the instance list, then:
238+
curl https://<EXTERNAL-IP>/
239+
# -> Hello from Datum (PHP)
240+
241+
curl https://<EXTERNAL-IP>/healthz
242+
# -> ok
243+
```
244+
245+
Use `-k` if the TLS certificate is self-signed in your metro:
246+
247+
```sh
248+
curl -k https://<EXTERNAL-IP>/
249+
```
250+
251+
---
252+
253+
## 5. Update the workload
254+
255+
To deploy a new version, rebuild and publish the image (repeating step 2), then redeploy. Using the manifest:
256+
257+
```sh
258+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
259+
--buildkit-host docker-container://buildkit \
260+
deploy --no-start -M 1024 --name hello-php \
261+
--runtime base:latest --rootfs ./Dockerfile .
262+
263+
datumctl compute deploy -f workload.yaml -y
264+
```
265+
266+
Or with flags:
267+
268+
```sh
269+
datumctl compute deploy hello-php \
270+
--image=index.unikraft.io/datum/hello-php:latest \
271+
--city=DFW \
272+
--port=8080
273+
```
274+
275+
Watch the rollout progress:
276+
277+
```sh
278+
datumctl compute rollout hello-php
279+
```
280+
281+
---
282+
283+
## 6. Clean up
284+
285+
```sh
286+
# Delete the workload and all its instances.
287+
datumctl compute destroy hello-php -y
288+
289+
# Stop the local BuildKit daemon.
290+
docker rm -f buildkit
291+
```
292+
293+
---
294+
295+
## Troubleshooting
296+
297+
### The image fails to boot: "No space left on device"
298+
299+
```
300+
[libukcpio] ...: Failed to load content: No space left on device (28)
301+
[libposix_vfs_fstab] Failed to extract CPIO to /: -3
302+
```
303+
304+
The rootfs is too large for the unikernel's in-RAM filesystem. This happens if you copy the whole `/usr/lib/x86_64-linux-gnu` directory instead of the trimmed library closure. Use the `ldd`-driven closure in the Dockerfile above, and avoid copying static archives or bulk you don't use.
305+
306+
### The application fails with a missing shared library
307+
308+
If the console shows a library-not-found error at boot, an extension's dependency is missing from the closure. Re-run `ldd` over the relevant extension `.so` in PHP's `extension_dir` and confirm each dependency lands in `/rootfs-libs`. If you add a **PECL/third-party extension**, it ships its own `.so` with its own library dependencies — those must be present in the rootfs too.
309+
310+
### Instance shows `Ready` but the endpoint doesn't respond
311+
312+
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:
313+
314+
```sh
315+
kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \
316+
instance logs <ukc-instance-name>
317+
```
318+
319+
A healthy boot prints PHP's `PHP <ver> Development Server (http://0.0.0.0:8080) started` line. A boot error (the rootfs-size or missing-library cases above) appears here. The `<ukc-instance-name>` appears in the instance's details from `datumctl compute instances describe <instance-name>`.
320+
321+
### Image pull failures on the instance
322+
323+
`datumctl compute instances describe <instance-name>` reports a condition with reason `ImageUnavailable` when the platform cannot pull the image. Confirm:
324+
325+
- 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.
326+
- The `kraft cloud deploy` command completed without errors and printed the image reference.
327+
- The image name in `workload.yaml` matches exactly what `kraft cloud deploy` reported, including the `latest` tag.
328+
329+
### Instance is stuck and not progressing
330+
331+
```sh
332+
datumctl compute instances describe <instance-name>
333+
```
334+
335+
Look at the conditions in the output. Common states:
336+
337+
- `QuotaGranted: False` — compute quota has not been provisioned for the project. Contact your platform operator.
338+
- `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.
339+
- `Ready: False, reason: SchedulingGatesPresent` — a scheduling prerequisite (such as a network) has not been satisfied. Confirm your project has a `default` Network resource provisioned.

0 commit comments

Comments
 (0)