diff --git a/docs/guides/deploy-a-php-app.md b/docs/guides/deploy-a-php-app.md new file mode 100644 index 00000000..0b34a185 --- /dev/null +++ b/docs/guides/deploy-a-php-app.md @@ -0,0 +1,340 @@ +# Deploy a PHP Web Service on Datum Compute + +> Last verified: 2026-06-02 against the `hello-php` example and the live `kraft` / `datumctl compute` CLIs. +> The complete, ready-to-deploy example for this guide lives in [`examples/hello-php/`](../../examples/hello-php/). + +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: + +- A PHP application 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. +- PHP (for local development only — the build happens inside Docker). + +--- + +## 1. Write the application + +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. + +**`server.php`** + +```php +/dev/null \ + | awk '/=>/ {print $3}' \ + | grep -E '^/(usr/)?lib' \ + | sort -u > /tmp/sonames.txt; \ + while read -r p; do \ + [ -n "$p" ] || continue; \ + real="$(readlink -f "$p")"; \ + cp -a "$real" "/rootfs-libs/$(basename "$real")"; \ + if [ "$(basename "$p")" != "$(basename "$real")" ]; then \ + ln -sf "$(basename "$real")" "/rootfs-libs/$(basename "$p")"; \ + fi; \ + done < /tmp/sonames.txt; \ + du -sh /rootfs-libs + +FROM scratch + +# The php CLI binary. +COPY --from=base /usr/local/bin/php /usr/local/bin/php + +# The bundled extensions. Preserve the exact extension_dir path so PHP resolves +# them without an explicit override. +COPY --from=base /usr/local/lib/php /usr/local/lib/php + +# glibc dynamic loader (the program interpreter named in the php ELF header). +COPY --from=base /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 + +# The precise shared-library closure, under both default loader search paths so +# NEEDED SONAMEs resolve without an ld.so.cache (intentionally not copied: it +# references libraries we did not ship; the loader falls back to its default +# trusted search paths, where these libraries live). +COPY --from=base /rootfs-libs/ /lib/x86_64-linux-gnu/ +COPY --from=base /rootfs-libs/ /usr/lib/x86_64-linux-gnu/ + +COPY ./server.php /server.php +``` + +> **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-/`); copying the whole `/usr/local/lib/php` tree preserves that path so PHP finds them. + +### Write the Kraftfile + +```yaml +spec: v0.6 + +runtime: base:latest + +rootfs: ./Dockerfile + +cmd: ["/usr/local/bin/php", "-S", "0.0.0.0:8080", "/server.php"] +``` + +`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.) + +### 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. It pushes to `index.unikraft.io/datum/`. The `-M` flag sets the memory allocation in MiB and is required — use at least `1024`. + +```sh +export KRAFTKIT_NO_CHECK_UPDATES=true + +kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \ + --buildkit-host docker-container://buildkit \ + deploy --no-start -M 1024 --name hello-php \ + --runtime base:latest --rootfs ./Dockerfile . +``` + +After this command completes, your image is available at `index.unikraft.io/datum/hello-php: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-php + labels: + app: hello-php +spec: + template: + metadata: + labels: + app: hello-php + spec: + runtime: + resources: + instanceType: datumcloud/d1-standard-2 + sandbox: + containers: + - name: app + image: index.unikraft.io/datum/hello-php: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-php \ + --image=index.unikraft.io/datum/hello-php: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-php +``` + +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 (PHP) + +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 publish 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 1024 --name hello-php \ + --runtime base:latest --rootfs ./Dockerfile . + +datumctl compute deploy -f workload.yaml -y +``` + +Or with flags: + +```sh +datumctl compute deploy hello-php \ + --image=index.unikraft.io/datum/hello-php:latest \ + --city=DFW \ + --port=8080 +``` + +Watch the rollout progress: + +```sh +datumctl compute rollout hello-php +``` + +--- + +## 6. Clean up + +```sh +# Delete the workload and all its instances. +datumctl compute destroy hello-php -y + +# Stop the local BuildKit daemon. +docker rm -f buildkit +``` + +--- + +## Troubleshooting + +### The image fails to boot: "No space left on device" + +``` +[libukcpio] ...: Failed to load content: No space left on device (28) +[libposix_vfs_fstab] Failed to extract CPIO to /: -3 +``` + +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. + +### The application fails with a missing shared library + +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. + +### 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 PHP's `PHP Development Server (http://0.0.0.0:8080) started` line. A boot error (the rootfs-size or missing-library cases above) appears here. 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 `ImageUnavailable` when the platform cannot pull 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-php/.gitignore b/examples/hello-php/.gitignore new file mode 100644 index 00000000..840bb308 --- /dev/null +++ b/examples/hello-php/.gitignore @@ -0,0 +1,2 @@ +# Build artifacts produced locally by kraft; not committed. +.unikraft/ diff --git a/examples/hello-php/Dockerfile b/examples/hello-php/Dockerfile new file mode 100644 index 00000000..46949694 --- /dev/null +++ b/examples/hello-php/Dockerfile @@ -0,0 +1,81 @@ +# Dynamically-linked glibc PHP runtime proof. +# +# Like the CPython proof (and unlike the static-PIE Go/Rust proofs), the PHP CLI +# is a dynamically-linked glibc ELF: it needs its dynamic loader +# (/lib64/ld-linux-x86-64.so.2) and a set of glibc shared objects at runtime, +# plus the shared libraries that its bundled extensions (the *.so files in PHP's +# extension_dir) link against (e.g. libssl/libcrypto, libxml2, libz, libargon2, +# libsodium). The bet, already proven for Python: the base:latest app-elfloader +# runs a dynamic ELF when the loader and every needed .so are present in the +# rootfs. So we ship them all -- but only the precise closure. +# +# IMPORTANT -- rootfs size matters: the unikernel extracts its rootfs into an +# in-RAM filesystem at boot. Shipping the WHOLE /usr/lib/x86_64-linux-gnu +# directory overflows that RAM disk and the boot FAILS with: +# [libukcpio] ...: Failed to load content: No space left on device (28) +# [libposix_vfs_fstab] Failed to extract CPIO to /: -3 +# So we copy ONLY the precise shared-library closure the php binary and its +# bundled extensions actually need, computed at build time by walking ldd. +# +# Stage 1 is the upstream PHP CLI image pinned to an exact patch version. The +# closure is computed by walking ldd over /usr/local/bin/php AND every bundled +# extension *.so in extension_dir, then staging each needed library plus its +# SONAME symlink into /rootfs-libs. Stage 2 is a FROM scratch rootfs holding the +# php binary, the extensions dir (at its exact extension_dir path so PHP finds +# them), the glibc loader, and just that library closure. +FROM php:8.3.14-cli-bookworm AS base + +# Build the exact shared-library closure for the php binary + every bundled +# extension .so. Walking ldd over each extension captures libraries the +# extensions link (e.g. libssl/libcrypto via openssl, libxml2 via dom/xml, +# libsodium, libargon2) that a hand-written list would miss. SONAME symlinks are +# preserved so the loader resolves NEEDED entries; real targets are copied +# alongside. +RUN set -eu; \ + extdir="$(php -r 'echo ini_get("extension_dir");')"; \ + echo "extension_dir=$extdir"; \ + mkdir -p /rootfs-libs; \ + { \ + ldd /usr/local/bin/php; \ + for f in "$extdir"/*.so; do [ -e "$f" ] && ldd "$f"; done; \ + } 2>/dev/null \ + | awk '/=>/ {print $3}' \ + | grep -E '^/(usr/)?lib' \ + | sort -u > /tmp/sonames.txt; \ + while read -r p; do \ + [ -n "$p" ] || continue; \ + real="$(readlink -f "$p")"; \ + cp -a "$real" "/rootfs-libs/$(basename "$real")"; \ + if [ "$(basename "$p")" != "$(basename "$real")" ]; then \ + ln -sf "$(basename "$real")" "/rootfs-libs/$(basename "$p")"; \ + fi; \ + done < /tmp/sonames.txt; \ + echo "=== staged library closure ==="; ls -la /rootfs-libs; \ + echo "=== closure size ==="; du -sh /rootfs-libs + +# Record the php binary's direct ldd closure + ELF shape in the build log. +RUN echo "=== ldd /usr/local/bin/php ===" && ldd /usr/local/bin/php && \ + echo "=== file php ===" && file /usr/local/bin/php + +FROM scratch + +# The php CLI binary. +COPY --from=base /usr/local/bin/php /usr/local/bin/php + +# The bundled extension .so files. Preserve the EXACT extension_dir path so PHP +# resolves them without an explicit -d extension_dir override. For 8.3 CLI this +# is /usr/local/lib/php/extensions/no-debug-non-zts-/. +COPY --from=base /usr/local/lib/php /usr/local/lib/php + +# glibc dynamic loader (the program interpreter named in the php ELF header). +COPY --from=base /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 + +# The precise shared-library closure, staged in stage 1. We place it under BOTH +# default loader search paths (/lib/x86_64-linux-gnu and /usr/lib/...) so NEEDED +# SONAMEs resolve without an ld.so.cache. (ld.so.cache is intentionally NOT +# copied: it references libraries we did not ship; the loader falls back to the +# default trusted search paths, where these libraries live.) +COPY --from=base /rootfs-libs/ /lib/x86_64-linux-gnu/ +COPY --from=base /rootfs-libs/ /usr/lib/x86_64-linux-gnu/ + +COPY ./server.php /server.php diff --git a/examples/hello-php/Kraftfile b/examples/hello-php/Kraftfile new file mode 100644 index 00000000..87b1776d --- /dev/null +++ b/examples/hello-php/Kraftfile @@ -0,0 +1,24 @@ +# Kraftfile for the dynamically-linked glibc PHP runtime proof. +# +# Like the CPython proof (and unlike the static-PIE Go/Rust proofs), the rootfs +# ships the php CLI binary, its bundled extensions, the glibc dynamic loader, +# and the glibc shared-library closure. The bet, proven for Python: base:latest +# (app-elfloader) runs a dynamic ELF when the loader + every needed .so is +# present in the rootfs. `rootfs: ./Dockerfile` makes kraft build that rootfs +# from the multi-stage Dockerfile. +# +# The cmd runs PHP's built-in web server, which binds the literal 0.0.0.0:8080 +# (php -S does not read $PORT), serving every request through /server.php. +# +# Build/push (push-only, do not start): +# kraft cloud --metro /v1 --token \ +# --buildkit-host docker-container://buildkit \ +# deploy --no-start -M 1024 --name hello-php \ +# --runtime base:latest --rootfs ./Dockerfile . +spec: v0.6 + +runtime: base:latest + +rootfs: ./Dockerfile + +cmd: ["/usr/local/bin/php", "-S", "0.0.0.0:8080", "/server.php"] diff --git a/examples/hello-php/README.md b/examples/hello-php/README.md new file mode 100644 index 00000000..7263a69e --- /dev/null +++ b/examples/hello-php/README.md @@ -0,0 +1,73 @@ +# hello-php + +A minimal PHP HTTP service packaged as a Unikraft unikernel and deployed on +Datum compute. It responds `Hello from Datum (PHP)` on `/` and `ok` on +`/healthz`, served by PHP's built-in web server on `0.0.0.0:8080`. No +third-party dependencies — just the stock `php` CLI and its bundled extensions. + +This is the runnable companion to the step-by-step guide: +[Deploy a PHP Web Service on Datum Compute](../../docs/guides/deploy-a-php-app.md). + +## How this differs from hello-go / hello-rust + +The Go and Rust examples ship a single fully static **PIE** binary with no +interpreter — the app *is* the unikernel entrypoint. The PHP CLI is different +(and identical in shape to `hello-python`): it is a **dynamically-linked glibc +ELF** that needs its dynamic loader (`/lib64/ld-linux-x86-64.so.2`) and a set of +glibc shared objects at runtime, plus the libraries its bundled extensions link +against. + +This example proves that the `base:latest` app-elfloader runtime **does** boot a +dynamic glibc executable, provided the loader and every needed `.so` are present +in the rootfs (the same `base:latest` runtime as Go/Rust/Python). + +Two things are load-bearing in the `Dockerfile`: + +1. **Ship the loader + the shared-library closure.** The rootfs includes the + `php` binary, its bundled extensions (`extension_dir`), the glibc loader, and + the closure of shared libraries that the binary *and* every bundled + extension link at runtime (e.g. `libssl`/`libcrypto`, `libxml2`, `libz`, + `libsodium`, `libargon2`). The closure is computed at build time by walking + `ldd` over the `php` binary and each extension `*.so`. +2. **Ship ONLY that closure, not the whole `/usr/lib/x86_64-linux-gnu`.** The + unikernel extracts its rootfs into an in-RAM filesystem at boot. Copying the + entire glibc lib directory overflows that RAM disk and the boot **fails** + with `[libukcpio] ...: No space left on device (28)` / + `[libposix_vfs_fstab] Failed to extract CPIO to /: -3`. The trimmed closure + boots in tens of milliseconds. + +The PHP patch version is pinned (`php:8.3.14-cli-bookworm`) so the copied +extensions match the `php` binary's extension API. + +## Files + +- `server.php` — the router script for PHP's built-in web server. +- `Dockerfile` — multi-stage build; stage 1 stages the exact shared-library + closure, stage 2 is a `FROM scratch` rootfs with the `php` binary, its + extensions, the loader, and that closure. +- `Kraftfile` — runs `php -S 0.0.0.0:8080 /server.php` on `base:latest`. +- `workload.yaml` — the Datum compute Workload manifest. + +## Quick start + +```sh +# 1. Build and publish the image (kraft builds + pushes; it does not run it). +# -M 1024: PHP needs more memory than the static Go/Rust binaries. +kraft cloud --metro "$UKC_METRO" --token "$UKC_TOKEN" \ + --buildkit-host docker-container://buildkit \ + deploy --no-start -M 1024 --name hello-php \ + --runtime base:latest --rootfs ./Dockerfile . + +# 2. Deploy on Datum compute. +datumctl compute deploy -f workload.yaml -y + +# 3. Verify. +datumctl compute instances --workload=hello-php +curl -k https:/// # -> Hello from Datum (PHP) +curl -k https:///healthz # -> ok +``` + +A healthy boot prints PHP's own +`PHP 8.3.x Development Server (http://0.0.0.0:8080) started` line on the +unikernel console (`kraft cloud instance logs `). `php -S` +binds the literal `0.0.0.0:8080`, so the workload port must be `8080` to match. diff --git a/examples/hello-php/server.php b/examples/hello-php/server.php new file mode 100644 index 00000000..8075cb01 --- /dev/null +++ b/examples/hello-php/server.php @@ -0,0 +1,18 @@ + "ok" +// anything else -> "Hello from Datum (PHP)" +// PHP's built-in server prints its own boot marker to the console on start: +// "PHP Development Server (http://0.0.0.0:8080) started" +// so no extra startup print is required here. + +header('Content-Type: text/plain; charset=utf-8'); + +$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); + +if ($path === '/healthz') { + echo "ok\n"; +} else { + echo "Hello from Datum (PHP)\n"; +} diff --git a/examples/hello-php/workload.yaml b/examples/hello-php/workload.yaml new file mode 100644 index 00000000..a08666f2 --- /dev/null +++ b/examples/hello-php/workload.yaml @@ -0,0 +1,33 @@ +apiVersion: compute.datumapis.com/v1alpha +kind: Workload +metadata: + name: hello-php + labels: + app: hello-php +spec: + template: + metadata: + labels: + app: hello-php + spec: + runtime: + resources: + instanceType: datumcloud/d1-standard-2 + sandbox: + containers: + - name: app + image: index.unikraft.io/datum/hello-php:latest + ports: + - name: http + port: 8080 + protocol: TCP + networkInterfaces: + - network: + name: default + placements: + - name: default + cityCodes: + - DFW + scaleSettings: + minReplicas: 1 + instanceManagementPolicy: OrderedReady