Skip to content

Commit 8288bdf

Browse files
committed
docs: add PHP deploy guide and runnable example for Datum compute
1 parent 5852ed0 commit 8288bdf

7 files changed

Lines changed: 571 additions & 0 deletions

File tree

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

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

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

0 commit comments

Comments
 (0)