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
pingopens, which needsCAP_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 runworks. - The Space CLI, installed as the documentation home shows.
Push the image
Check that the image runs with your usual OCI tool:
docker run --rm ghcr.io/<your-org>/<your-app>:latestThe 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:
docker push ghcr.io/<your-org>/<your-app>:latestA 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.projectversion: "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.sockThe 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:
$ 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 0Run 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:
space build . -t my-app:devspace run my-app:devIn 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:
$ 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:
space build . -t ghcr.io/<your-org>/my-app:release --release \ --signing-key release --rollback-index 1Connect 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:
$ 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 reachablePrint the deployment plan, then deploy:
space deploy ghcr.io/<your-org>/my-app:release --target sat-42 --planspace 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.