---
title: "How to Run Java in a Sandbox"
description: "Bake the JDK into a VM snapshot, boot one VM, and run many Java programs on it with an isolated, reusable runJava() helper — then serve a Java HTTP server from the same snapshot over HTTPS."
url: "/docs/guides/run-java-in-a-sandbox"
---

This guide builds a reusable snapshot with the JDK preinstalled, boots one VM from it, then wraps that VM in a `runJava()` helper you can call as many times as you like — each call writes to a uniquely-named file, so runs never clash, sequentially or concurrently. The same snapshot also powers a long-running HTTP server you can reach over HTTPS.


## Install the SDK

```bash pnpm
pnpm add freestyle
```

```bash bun
bun add freestyle
```

```bash npm
npm install freestyle
```

```bash yarn
yarn add freestyle
```

Set your API key before calling the API:

```bash
export FREESTYLE_API_KEY="your-api-key"
```


## Build a Snapshot with Java

Create a VM from the minimal base image and install [Amazon Corretto](https://aws.amazon.com/corretto/) — a free, production-ready OpenJDK build — from its apt repository, then snapshot the machine so the install is baked in. Add Corretto's signing key and repo, then install the JDK. The minimal image also lacks `/usr/share/man/man1`, which the JDK post-install configures into — create it first so the install finishes cleanly. Delete the builder VM once the snapshot is captured.

```ts
import { freestyle } from "freestyle";

const { vm: builder } = await freestyle.vms.create();

// Add the Amazon Corretto apt repository (needs gnupg for the signing key).
await builder.exec(
  "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gnupg ca-certificates",
);
await builder.exec(
  "curl -fsSL https://apt.corretto.aws/corretto.key | gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg && " +
    "echo 'deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main' > /etc/apt/sources.list.d/corretto.list",
);

// Install Corretto 21 (the man dir must exist for the JDK post-install step).
await builder.exec("mkdir -p /usr/share/man/man1");
const install = await builder.exec(
  "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq java-21-amazon-corretto-jdk",
);
console.log(install.statusCode); // 0

const version = await builder.exec("java -version 2>&1");
console.log(version.stdout); // OpenJDK Runtime Environment Corretto-21.0.11.10.1 ...

const { snapshotId } = await builder.snapshot();
await builder.delete();
```


## A Reusable runJava() Utility

The VM is the reusable sandbox: create it once, then run as many snippets as you like on it. The helper takes the `vm` as its first argument, writes your source to a uniquely-named file, and runs it with the JDK 21 single-file launcher (the filename need not match the public class). Each call writes to `/tmp/main-<uuid>.java` via `crypto.randomUUID()`, so no two runs — sequential or concurrent — can clobber each other's source. The helper never creates or deletes a VM; lifecycle stays with the caller.

```ts
async function runJava(vm, code: string) {
  const file = `/tmp/main-${crypto.randomUUID()}.java`;
  await vm.fs.writeTextFile(file, code);
  return await vm.exec(`java ${file}`);
}

const { vm } = await freestyle.vms.create({ snapshotId });

const result = await runJava(vm, `public class Main {
  public static void main(String[] args) {
    System.out.println("Hello from Java " + System.getProperty("java.version"));
  }
}
`);

console.log(result.statusCode); // 0
console.log(result.stdout); // Hello from Java 21.0.11
```

Each `exec` returns `{ stdout, stderr, statusCode }`, where a `statusCode` of `0` means the program exited successfully. Reuse the same `vm` for the next snippet — there is no need to boot a new machine.


## Pass Arguments and Standard Input

The base `runJava` is great for quick snippets, but it hands back a raw result and never checks `statusCode`, so a failed program looks the same as a successful one. Add a second, stricter helper — `runJavaStrict` — that also accepts command-line `args` and `stdin`, returns a typed result, and throws when the program exits non-zero so failures surface as exceptions instead of silent bad output. It is a distinct function with its own name, so both helpers can live side by side in the same script. Like `runJava`, it takes the `vm` first and reuses it — only the per-call source and stdin files are unique.

```ts
async function runJavaStrict(
  vm,
  code: string,
  { args = [], stdin = "" }: { args?: (string | number)[]; stdin?: string } = {},
): Promise<{ stdout: string; stderr: string; statusCode: number }> {
  const id = crypto.randomUUID();
  const file = `/tmp/main-${id}.java`;
  const stdinFile = `/tmp/stdin-${id}`;
  await vm.fs.writeTextFile(file, code);
  if (stdin) await vm.fs.writeTextFile(stdinFile, stdin);

  const argv = args
    .map((a) => `'${String(a).replace(/'/g, "'\\''")}'`)
    .join(" ");
  const cmd = stdin
    ? `java ${file} ${argv} < ${stdinFile}`
    : `java ${file} ${argv}`;

  const r = await vm.exec(cmd);
  if (r.statusCode !== 0) {
    throw new Error(`runJavaStrict: java exited ${r.statusCode}: ${r.stderr ?? r.stdout}`);
  }
  return { stdout: r.stdout ?? "", stderr: r.stderr ?? "", statusCode: r.statusCode };
}
```

Run several snippets on the **same** `vm`. Pass an argument to compute a factorial:

```ts
const fact = await runJavaStrict(vm, `public class Main {
  public static void main(String[] args) {
    long n = Long.parseLong(args[0]), f = 1;
    for (long i = 2; i <= n; i++) f *= i;
    System.out.println(n + "! = " + f);
  }
}
`, { args: [10] });

console.log(fact.stdout); // 10! = 3628800
```

Feed input on stdin and read it with a `Scanner`:

```ts
const sum = await runJavaStrict(vm, `import java.util.Scanner;
public class Main {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int total = 0;
    while (sc.hasNextInt()) total += sc.nextInt();
    System.out.println("sum=" + total);
  }
}
`, { stdin: "3 4 5 6\n" });

console.log(sum.stdout); // sum=18
```

Because every run uses a unique filename, you can even fire several at once on the one VM — they stay fully isolated:

```ts
const [a, b] = await Promise.all([
  runJavaStrict(vm, `public class Main {
    public static void main(String[] x) { System.out.println("run-a"); }
  }`),
  runJavaStrict(vm, `public class Main {
    public static void main(String[] x) { System.out.println("run-b"); }
  }`),
]);

console.log(a.stdout, b.stdout); // run-a  run-b
```

A program that throws now rejects the promise, so a `try`/`catch` around `runJavaStrict()` cleanly handles a failed run.


## Run a Server

The same snapshot that runs one-off snippets can host a long-running HTTP server. Create a fresh VM from `snapshotId` — give it a distinct name like `server` so it lives alongside the run-code `vm` — and pass `idleTimeoutSeconds: null` so it never idles out from under the service.

```ts
const { vm: server, vmId } = await freestyle.vms.create({
  snapshotId,
  idleTimeoutSeconds: null,
});
```

Write the server source to `/srv`. This uses the built-in `com.sun.net.httpserver.HttpServer`, so there are no dependencies to fetch. It binds to `0.0.0.0` (not `127.0.0.1`) so traffic from outside the VM can reach it.

```ts
await server.fs.writeTextFile(
  "/srv/Server.java",
  `import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class Server {
  public static void main(String[] args) throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 3000), 0);
    server.createContext("/", exchange -> {
      byte[] body = "Hello from a Java server in a sandbox!\\n".getBytes("UTF-8");
      exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
      exchange.sendResponseHeaders(200, body.length);
      try (OutputStream os = exchange.getResponseBody()) {
        os.write(body);
      }
    });
    server.setExecutor(null);
    server.start();
  }
}
`,
);
```

Write a systemd unit so the server starts on boot and restarts if it crashes. systemd units do not source a shell profile, so `ExecStart` uses the absolute `java` path (`/usr/bin/java`) and launches the source file directly with the JDK single-file launcher — no separate compile step.

```ts
await server.fs.writeTextFile(
  "/etc/systemd/system/java-server.service",
  `[Unit]
Description=Java HTTP server
After=network.target

[Service]
ExecStart=/usr/bin/java /srv/Server.java
WorkingDirectory=/srv
Restart=always

[Install]
WantedBy=multi-user.target
`,
);
```

Enable and start the service, then poll until it answers `200` on localhost so you know it is live before routing traffic to it.

```ts
await server.exec("systemctl daemon-reload && systemctl enable --now java-server");

let ready = false;
for (let i = 0; i < 60; i++) {
  const probe = await server.exec(
    "curl -s -o /dev/null -w '%{http_code}' http://localhost:3000 || true",
  );
  if (probe.stdout.trim() === "200") {
    ready = true;
    break;
  }
  await new Promise((resolve) => setTimeout(resolve, 1000));
}
if (!ready) throw new Error("server never became ready");
```

Map a domain to the VM's port with `freestyle.domains.mappings.create` — pick your own unique `style.dev` subdomain in place of `my-app`, since `style.dev` domains need no verification and get HTTPS automatically.

```ts
const domain = `my-app-${crypto.randomUUID().slice(0, 8)}.style.dev`;

await freestyle.domains.mappings.create({ domain, vmId, vmPort: 3000 });
```

Fetch it from outside the VM:

```ts
const res = await fetch(`https://${domain}`);
console.log(res.status); // 200
console.log(await res.text()); // Hello from a Java server in a sandbox!
```

One snapshot now serves two purposes: a sandbox for running arbitrary Java snippets and a base image for a public HTTPS service — boot a VM from it any time and the JDK is ready with no install cost.


## Stream the Server's Logs

`vm.exec()` buffers a command and only returns once it finishes, so it can't show a long-running service's output as it happens. To watch the logs live, open a [PTY](https://www.freestyle.sh/docs/vms/pty) on the VM — a real terminal streamed over a WebSocket (server-side only, Node 22+) — and follow the unit's journal. `onData` delivers the bytes as they arrive:

```ts
const session = await server.pty.open({
  cols: 120,
  rows: 30,
  onData: (bytes) => process.stdout.write(bytes), // live log lines
});

// Follow the service; new lines stream in until you detach.
session.write("journalctl -u java-server -f\n");

// session.detach() drops your handle — the service keeps running in the VM.
```
