Skip to content
The Homebrew tap serves no working release today, and the build, run and deploy steps need the Space CLI and, for the last one, a connected board.

Port a Linux app

Run an existing OCI image on SpaceOS as a Linux service. Describe where it runs in space.yaml, run it on your computer, then deploy it to a connected target.

A Linux service runs under runc, the reference OCI container runtime. runc unpacks the image's root filesystem and starts its process inside fresh Linux namespaces and cgroups, so the service shares its partition's kernel. The isolation layers chapter compares that boundary with a unikernel's.

What a Linux service runs without

A Linux service starts with no Linux capabilities. Its bounding, permitted, effective, inheritable, and ambient sets are empty, and no_new_privileges stops a setuid program from raising them. A user namespace maps root inside the container to an unprivileged user on the node. The process runs under Docker's default seccomp filter. The kernel paths containerd masks, such as /proc/kcore, are hidden, and the ones it keeps read-only stay so. The container holds the default device nodes of an OCI runtime, such as /dev/null and /dev/urandom, and no device of the node.

An image fails at run time when it needs any of these:

  • a raw or packet socket, such as the one ping opens, which needs CAP_NET_RAW;
  • a mount, or another operation that needs CAP_SYS_ADMIN;
  • a device node of the node, such as a GPU's /dev/nvidia*, /dev/kfd, or /dev/dri/*.

A service binds a port below 1024 inside its own network namespace without a capability. A service that publishes such a port on the node through a host_network interface keeps CAP_NET_BIND_SERVICE and no other capability, and runs without the user namespace. No field of space.yaml grants another capability or a device. An image that fails under docker run --cap-drop=ALL --security-opt no-new-privileges fails here too.

Prerequisites

  • An OCI image for your application. Any image that runs under docker run works.
  • The Space CLI, installed as the documentation home shows.

Push the image

Check that the image runs with your usual OCI tool:

Terminal window
docker run --rm ghcr.io/<your-org>/<your-app>:latest

The Space CLI resolves a service image by its OCI reference. A registry is an HTTP server that stores OCI images and serves them by name and tag or by digest, such as ghcr.io or Docker Hub. Push the image to a registry that your computer and your target can both reach:

Terminal window
docker push ghcr.io/<your-org>/<your-app>:latest

A private registry needs credentials on the target for the reference you name in space.yaml.

Describe the service

Create a project with a space.yaml that places the service:

schema: space.project
version: "1"
build:
output: _build/spaceos
run:
partitions:
payload:
init: pid1
disk: true
memory: 512
apids: [0x010, 0x01F]
services:
- name: my-app
image: ghcr.io/<your-org>/<your-app>:latest
runtime: oci
isolation: runc
cpus: [0]
healthcheck:
type: command
argv: ["/bin/sh", "-lc", "test -f /tmp/ready"]
relay:
from: payload
to: [ground]
ground:
port: 8080
socket: /tmp/spaceos/ground.sock

The build: section names where the image is written. The run: section places the service: its memory, its APID range, the relay to the ground, and the ground endpoint. isolation: runc makes it a Linux service. The space.yaml reference describes every field.

Check the file before you build. space show --sbom decodes the project, and space run --dry-run resolves the placement and prints it:

Terminal window
$ space show --sbom | sed -n '1p;4p'
No signed SBOM at _build/spaceos/spaceos.spdx.json yet; printing the SBOM computed from space.yaml
kernel ghcr.io/parsimoni-labs/uniboot-kernel/node:6.18.39-62332cf6bcc6@sha256:21e9b2669e2347dd0a9b7703a248bf191ef91e412ae6b720f3d3768f6cfbbd8f
$ space run --dry-run | sed -n '4,7p'
partitions:
payload 512 MB apids 0x010, 0x01f
services:
my-app runc cpus 0

Run it on your computer

Build the project and boot it in the local VM. The first build creates a development signing identity and prints its fingerprint:

Terminal window
space build . -t my-app:dev
space run my-app:dev

In another terminal, space status lists the service and space logs shows what it prints.

Deploy to a target

A release build needs a release signing identity. space build --release refuses the development identity and does not create the one you name, so generate it first:

Terminal window
$ space keys generate release
✓ Generated signing identity release
ed25519-key: /tmp/space-docs/config/keys/release.ed25519.pem
mldsa65-key: /tmp/space-docs/config/keys/release.mldsa65.pem
fingerprint: sha256:...

A signing identity holds two key pairs, and a release build signs with both: Ed25519, the classical scheme, and ML-DSA-65, the post-quantum scheme of FIPS 204. A node's trust root can require both signatures, and a forger then has to break both schemes. The signing chapter describes what each half signs.

A flight release is a --release build meant for a real target. It names an identity generated on the machine where releases are authorized, never the development one. --rollback-index must be 1 or more on a release, and a target refuses a release whose index is below one it already runs:

Terminal window
space build . -t ghcr.io/<your-org>/my-app:release --release \
--signing-key release --rollback-index 1

Connect the target once. --profile names its content-addressed target profile. A target profile is a YAML file in the target catalog that describes the board, its boot chain, its isolation backends, and its security claims. Its identity, printed as cm5-dev@sha256:..., is the SHA-256 of its canonical JSON form, which pins the digests of its companion files, so a change to any of them changes the identity.

--signing-key is the board's RSA-2048 secure-boot key, a different key from the release identity above. space deploy signs the per-board boot image with it as boot.sig, the signature the Raspberry Pi firmware checks. --endpoint and --cla-address give the node's delay-tolerant network name and the address that reaches it. --always-reachable suits a bench board on your network. A node reached over real passes names its orbit with --tle, a two-line element set, and its ground station with --station:

Terminal window
$ space target connect sat-42 --profile cm5-dev \
> --signing-key ./keys/sat-42.pem \
> --endpoint ipn:7.1 --cla-address sat-42.local:4556 --always-reachable
✓ sat-42 registered: cm5-dev, always reachable

Print the deployment plan, then deploy:

Terminal window
space deploy ghcr.io/<your-org>/my-app:release --target sat-42 --plan
space deploy ghcr.io/<your-org>/my-app:release --target sat-42

--plan prints the signed release descriptor and changes nothing on the target. space deploy assembles the release for the target's board and records it. space show sat-42 v1 reads that deploy back. Omit --target to deploy to the default target that space target ls marks.

When to move to a unikernel

Stay on a Linux service while your workload needs a full Linux userland, a large dependency tree, or plugins loaded at run time. A unikernel carries less code that must be correct for the system to be secure, starts faster from cold because it boots no general-purpose kernel and no init system, and runs in its own hardware virtual machine. Build a unikernel covers that path.