Skip to content

Deploying

An app that uses the SDK deploys as a static site. Your users’ Linux programs run in their browsers, so your server only serves files. With the hosted runtime frame, those are only your app’s own files: the engine and the default Linux image load from https://run.oxidevm.dev. What your host has to get right is the headers, because the page must be cross-origin isolated.

  1. Install the SDK:

    Terminal window
    npm install @oxidevm/sdk
  2. Create VMs with runtime: { hosting: "frame" }.

  3. Send Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on every response, from your development server and from your host.

  4. If your app sends a Content Security Policy, allow the runtime frame and the preview origin in frame-src; see Content Security Policy.

  5. Build and deploy, then open the deployed app and create a VM once.

main.ts
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", runtime: { hosting: "frame" } });

With runtime: { hosting: "frame" }, the SDK adds a hidden frame from https://run.oxidevm.dev to your page and runs the engine there, together with its workers and the image downloads. Your page talks to the frame over a MessagePort, and the VM you get has the same methods as in any other setup.

  • Your build contains no engine or image files, so there is nothing to stage, and nothing to keep in step with the SDK version.
  • The page itself must be cross-origin isolated. Otherwise createVM rejects with FEATURE_NOT_AVAILABLE, detail.reason: "browser-unsupported" and detail.requirement: "cross-origin-isolation".
  • Userspace JavaScript acceleration needs no setup: the frame provides its execution origin.
  • Previews need no setup either: VMs in the frame serve them from https://<session>.oxvm.dev unless you pass your own preview origin.
  • Your own command interceptor workers need a self-hosted engine; the frame runs only the packaged interceptor.
  • The frame downloads images you host yourself, so their server must allow it. See Custom images.
  • Public sites need a registered license token. Pass it as runtime: { hosting: "frame", license }. Missing or rejected tokens fail creation with PERMISSION_DENIED. Local development needs no token. See Licenses for registration, eligibility and renewal.

runtime.origin points the frame at another runtime origin. Leave it out for https://run.oxidevm.dev.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Every response of your app needs both headers: the HTML page, scripts, cached responses and error pages, and with a self-hosted engine also its worker, .wasm and image files. Without them the page is not cross-origin isolated, and the engine cannot share memory between its workers. Do not work around a missing header with a browser flag.

With require-corp, anything your page loads from another origin (fonts, scripts, images) needs a Cross-Origin-Resource-Policy header or CORS. Serve the app over HTTPS; http://localhost works during development. See Browser requirements.

vite.config.ts
import { defineConfig } from "vite";
const headers = {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
};
export default defineConfig({
optimizeDeps: { exclude: ["@oxidevm/sdk"] },
worker: { format: "es" },
server: { headers },
preview: { headers },
});

optimizeDeps.exclude stops Vite from pre-bundling the SDK, which would break the URLs of its workers. worker.format: "es" builds them as module workers. The headers cover vite dev and vite preview; set the same headers on your host. With a self-hosted engine, run assets install before vite build, because Vite copies public/ into the build output.

  • Create VMs only in client code: a "use client" component, inside useEffect or an event handler. Importing @oxidevm/sdk during server rendering is safe, but createVM needs a browser.
  • Add the headers with headers() in next.config.ts. With output: "export", Next.js does not apply headers(); set the headers on your static host instead.
next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
},
];
},
};
export default nextConfig;
app/linux-arch.tsx
"use client";
import { useEffect, useState } from "react";
import { createVM } from "@oxidevm/sdk";
export function LinuxArch() {
const [text, setText] = useState("Starting Linux");
useEffect(() => {
let active = true;
const opening = createVM({ mode: "userspace", runtime: { hosting: "frame" } });
opening
.then((vm) => vm.exec({ command: "uname", args: ["-m"] }))
.then((result) => {
if (active) setText(new TextDecoder().decode(result.stdout));
})
.catch((error: unknown) => {
if (active) setText(error instanceof Error ? error.message : "Linux did not start");
});
return () => {
active = false;
opening.then((vm) => vm.dispose()).catch(() => undefined);
};
}, []);
return <pre>{text}</pre>;
}

Hosts that read a _headers file from the published folder, such as Netlify and Cloudflare Pages, take the headers from public/_headers:

public/_headers
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

With the hosted frame, the engine compiles and runs inside the frame, under the frame’s own policy. If your app sends a Content Security Policy, it only has to allow the frames:

The SDK Directive
Embeds the hidden runtime frame https://run.oxidevm.dev in frame-src
Embeds hidden frames from the preview origin https://*.oxvm.dev in frame-src, or your own preview origin with a wildcard for per-session hosts

A starting point, to merge with what the rest of your app needs:

Content-Security-Policy: default-src 'self'; frame-src https://run.oxidevm.dev https://*.oxvm.dev

A self-hosted engine runs in your page and needs more; see its policy.

Licensed customers can serve an engine release from their own site instead of using the frame. The page then loads the engine directly, and your host serves the engine and image files next to your app.

  1. Install the CLI next to the SDK, and keep both on the same exact version:

    Terminal window
    npm install --save-dev @oxidevm/cli
  2. Add oxidevm.config.ts and run oxidevm assets install before every build.

  3. Create VMs with runtime: { hosting: "self-hosted", baseUrl: "/oxidevm/runtime/", license }.

  4. Send the isolation headers on every response, the engine and image files included.

  5. For Userspace, host an execution origin or create VMs with commandInterceptors: "off"; see Userspace JavaScript acceleration.

  6. Build and deploy the complete output, including the staged /oxidevm/ files and the SDK’s worker files.

  7. Run oxidevm doctor against the deployed URL.

The SDK package does not contain the engine. oxidevm assets install copies it, together with an image, into your static folder. Configure it in oxidevm.config.ts at your project root:

oxidevm.config.ts
import type { ProjectConfig } from "@oxidevm/sdk/image-builder";
export default {
mode: "userspace",
publicDir: "public",
assetBaseUrl: "/oxidevm/",
} satisfies ProjectConfig;
Field Default Meaning
mode Required "userspace" or "system": which default image to stage.
publicDir "public" Your bundler’s static folder.
assetBaseUrl "/oxidevm/" The URL path the files are served under. It must end with /.
Terminal window
npx oxidevm assets install

The command verifies and copies the engine release your SDK version pins (runtime/), the default image for mode (images/), and a few files that oxidevm doctor uses. The browser checks the engine files against the same pins before it runs them, so files from another SDK version fail with CORE_LOAD_FAILED.

The first run writes oxidevm.lock.json, which records the exact SDK, engine and image. Commit it. Later runs check the lock and never change it. Projects created with oxidevm init keep public/oxidevm/ out of Git, so run assets install in CI before the build. Next.js serves public/ at the site root, so the default publicDir and assetBaseUrl work there unchanged. To stage your own images, see Custom images.

After you upgrade the SDK and CLI, assets install stops with INVALID_ARGUMENT (“project lock differs from this exact SDK release”). Delete oxidevm.lock.json, run the command again, commit the new lock, and rebuild. If it then reports that a staged image differs from its pinned bytes, the new version pins another default image than the staged copy. Delete the folder that image.manifestUrl in oxidevm.lock.json points into, and run the command again.

self-hosted.ts
import { createVM } from "@oxidevm/sdk";
export function openVM(license: string) {
return createVM({
mode: "userspace",
runtime: { hosting: "self-hosted", baseUrl: "/oxidevm/runtime/", license },
commandInterceptors: "off", // until you host an execution origin, see below
});
}

/oxidevm/runtime/ is where assets install stages the engine with the default assetBaseUrl, and a createVM call without runtime loads the engine from there, without a license token. On localhost the engine runs without one, so leave license out of runtime during local development.

If you change assetBaseUrl, pass runtime: { hosting: "self-hosted", baseUrl: "<assetBaseUrl>runtime/" }, and for the default image image: defaultImage(mode, url), with the image.manifestUrl that oxidevm.lock.json records.

  • Serve .wasm files as application/wasm, scripts (workers included) as JavaScript, and the image .json files as application/json.
  • If your host answers unknown paths with index.html (a single-page-app fallback), exclude /oxidevm/ and your build’s script files from it. A missing file must return 404, not HTML.
  • Deploy the whole build output. The SDK starts its workers from files your bundler emits next to the main bundle.

Content Security Policy for a self-hosted engine

Section titled “Content Security Policy for a self-hosted engine”

If your app sends a Content Security Policy, add these sources to it:

The SDK Directive
Compiles WebAssembly, including code it generates while guest programs run 'wasm-unsafe-eval' in script-src
Starts module workers from your origin 'self' in worker-src
Loads engine and image files from your origin, and reads the verified engine modules through blob: URLs inside its workers 'self' and blob: in connect-src
Embeds hidden frames from your preview origin The preview origin in frame-src, with a wildcard for per-session hosts
Connects to a relay The relay in connect-src: http://127.0.0.1:7337 and ws://127.0.0.1:7337 for a personal relay, or your relay server’s https: and wss: origin

A starting point for an app with per-session previews, to merge with what the rest of your app needs:

Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; connect-src 'self' blob:; frame-src https://*.preview.example.com

The content-security-policy check of oxidevm doctor compiles the engine modules and starts a worker from a blob: URL, so the policy must also allow blob: workers for that check to pass.

Userspace VMs default to commandInterceptors: "auto", which runs some JavaScript processes in browser workers on a separate execution origin. The hosted frame provides one. A self-hosted engine finds it through native-node-api.json in the runtime folder: /oxidevm/runtime/native-node-api.json with the default layout. oxidevm assets install does not create this file.

To host an execution origin:

  • Serve the SDK’s assets/native-process/ folder at /oxidevm-native-process/ on a separate HTTPS origin that holds no application cookies, credentials or storage. Use the files of the SDK version your app uses.
  • Send Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Resource-Policy: cross-origin there, with a CSP of default-src 'none'; script-src 'self' 'unsafe-eval'; worker-src 'self'; connect-src 'none' plus frame-ancestors naming your app’s origin.
  • Put {"version":1,"origin":"https://exec.example.com"} in native-node-api.json next to the engine files, and allow that origin in your app’s frame-src.

See commandInterceptors for what the option changes.

Terminal window
npx oxidevm doctor https://app.example.com/
npx oxidevm doctor https://app.example.com/ --preview-origin https://preview.example.com --browser firefox --json

doctor opens the URL in a headless browser (--browser chromium, firefox or webkit; Chromium by default) and checks what the host actually serves. Without a URL it checks http://localhost:5173/. It exits with status 3 when a check fails, so CI can stop a release, and --json prints one JSON report.

Check Passes when
secure-context The page is served over HTTPS or from localhost.
cross-origin-isolation The page is cross-origin isolated and its response carries COOP same-origin and COEP require-corp.
wasm-features The browser has the features the SDK needs.
asset-integrity The staged engine files and doctor files match the installed SDK release.
wasm-mime The engine modules are served as application/wasm.
worker-load Module workers load from the staged files.
content-security-policy The page’s CSP allows WebAssembly compilation and a blob: worker.
storage The browser reports a storage quota and can write a test file.
preview-origin With --preview-origin, the preview files register their Service Worker; otherwise not-configured.

doctor finds the staged files through <meta name="oxidevm-assets" content="/oxidevm/"> in your page, and assumes /oxidevm/ without it. Add the tag when you change assetBaseUrl.

doctor loads its own copies of the SDK workers, so it does not prove that your bundler emitted the workers your app uses. Open the deployed app and create a VM once as well.

assets install stages custom images under images/<manifest digest>/, a new directory for every version. When you deploy a new version, keep the earlier directories on your host: restore points and durable workspaces can refer to the image they were made on. Publish each image directory complete, and never edit the files in it.