Sandbox SDK Sandbox SDK Sandbox SDK · Lab 5/7 Lab 5/7 Lab 5/7 · 30 phút · 30 min · 30 នាទី

05

Preview app trực tiếp Live App Preview Preview app ផ្ទាល់

Dựng môi trường dev trong sandbox: clone repo, cài deps, start server, proxy URL — một endpoint. Build a live development environment inside the sandbox. Clone a repo, install dependencies, start a dev server, and proxy the URL — all through a single endpoint. សង់បរិស្ថាន dev ក្នុង sandbox៖ clone repo ដំឡើង deps start server proxy URL — endpoint តែមួយ។

Nội dung bước (lệnh, code) giữ nguyên tiếng Anh từ nguồn chính thức. Step body (commands, code) stays in English from the official source. ខ្លឹមសារជំហាន (ពាក្យបញ្ជា និង code) រក្សាភាសាអង់គ្លេសពីប្រភពផ្លូវការ។ labs.cloudflare.dev ↗

Cần trước Prerequisites តម្រូវការជាមុន

  • Đã xong bước 4 Completed Step 4 បានបញ្ចប់ជំហាន 4
  • Sandbox đang chạy local Sandbox running locally Sandbox កំពុងរត់ local
  • Docker đang chạy Docker running Docker កំពុងរត់

Bạn sẽ làm được Learning objectives គោលបំណងសិក្សា

  • Clone repo vào sandbox bằng gitCheckout() Clone repositories into the sandbox with gitCheckout() Clone repo ចូល sandbox ដោយ gitCheckout()
  • Cài dependencies và chạy lệnh setup Install dependencies and run setup commands ដំឡើង dependencies និងរត់ពាក្យបញ្ជា setup
  • Start process chạy nền bằng startProcess() Start long-running background processes with startProcess() Start process ផ្ទៃក្រោយដោយ startProcess()
  • Expose port container và proxy request qua Worker Expose container ports and proxy requests through your Worker Expose port container និង proxy request តាម Worker
  • Stream log process để debug realtime Stream process logs for real-time debugging Stream log process ដើម្បី debug ផ្ទាល់

In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.

In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.

In this step, you’ll build a single POST /preview endpoint that clones a repository, installs dependencies, starts a dev server, and returns a live preview URL. You’ll add each piece incrementally — testing as you go — so you can see the endpoint grow from a simple clone into a full preview pipeline.

Bước 1: Scaffold endpoint preview Step 1: Scaffold the Preview Endpoint ជំហាន 1: Scaffold endpoint preview

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A `/preview` endpoint that clones a repository into the sandbox.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
gitCheckout() is the quickest way to get a full project into the sandbox for preview or testing.

Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:

Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:

Start by creating a POST /preview endpoint in src/index.ts that clones a sample Vite React app:

typescript
if (url.pathname === "/preview" && request.method === "POST") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");

  // Clone if not already present
  const { exists: alreadyCloned } = await previewSandbox.exists(
    "/workspace/vite-react",
  );

  if (!alreadyCloned) {
    await previewSandbox.gitCheckout(
      "https://github.com/harshil1712/vite-react.git",
    );
  }

  return Response.json({
    steps: {
      clone: {
        status: "success",
        message: alreadyCloned ? "Already cloned" : "Repository cloned",
      },
    },
  });
}

Test it:

Test it:

Test it:

bash
curl -X POST http://localhost:8787/preview
json
{
  "steps": {
    "clone": { "status": "success", "message": "Repository cloned" }
  }
}

The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.

The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.

The steps object will grow as you add each stage. This gives clear per-step feedback — if something fails later, you’ll see exactly which step broke.

dockerfile
FROM docker.io/cloudflare/sandbox:0.7.0

RUN git clone https://github.com/harshil1712/vite-react.git /workspace/vite-react
RUN cd /workspace/vite-react && npm install

EXPOSE 8080

Bước 2: Cài dependencies Step 2: Install Dependencies ជំហាន 2: ដំឡើង dependencies

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Dependencies installed in the sandbox so the application can run.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
Nearly every Node.js project requires npm install before it can start — this is no different inside a sandbox.

Add an install step to the same endpoint, right after the clone logic:

Add an install step to the same endpoint, right after the clone logic:

Add an install step to the same endpoint, right after the clone logic:

typescript
if (url.pathname === "/preview" && request.method === "POST") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");

  // 1. Clone if needed
  const { exists: alreadyCloned } = await previewSandbox.exists(
    "/workspace/vite-react",
  );

  if (!alreadyCloned) {
    await previewSandbox.gitCheckout(
      "https://github.com/harshil1712/vite-react.git",
    );
  }

  // 2. Install dependencies
  const install = await previewSandbox.exec("npm install", {
    cwd: "/workspace/vite-react",
    timeout: 120000, // 2 minutes — npm install can be slow
  });

  return Response.json({
    steps: {
      clone: {
        status: "success",
        message: alreadyCloned ? "Already cloned" : "Repository cloned",
      },
      install: {
        status: install.exitCode === 0 ? "success" : "failed",
        message:
          install.exitCode === 0 ? "Dependencies installed" : install.stderr,
      },
    },
  });
}

Test again — same endpoint, more information:

Test again — same endpoint, more information:

Test again — same endpoint, more information:

bash
curl -X POST http://localhost:8787/preview
json
{
  "steps": {
    "clone": { "status": "success", "message": "Already cloned" },
    "install": { "status": "success", "message": "Dependencies installed" }
  }
}

Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.

Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.

Notice the clone step now reports “Already cloned” on repeat calls — the idempotency check is working.

Bước 3: Start dev server Step 3: Start the Development Server ជំហាន 3: Start dev server

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A running Vite dev server inside the sandbox, started as a background process.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
startProcess() runs long-lived processes without blocking your Worker — the process keeps running after the HTTP response is sent.

The key difference from exec():

The key difference from exec():

The key difference from exec():

  • exec() — waits for the command to finish, then returns the result
  • startProcess() — starts the command and returns immediately with a process handle

Add the server start after install:

Add the server start after install:

Add the server start after install:

typescript
if (url.pathname === "/preview" && request.method === "POST") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");

  // 1. Clone if needed
  const { exists: alreadyCloned } = await previewSandbox.exists(
    "/workspace/vite-react",
  );

  if (!alreadyCloned) {
    await previewSandbox.gitCheckout(
      "https://github.com/harshil1712/vite-react.git",
    );
  }

  // 2. Install dependencies
  const install = await previewSandbox.exec("npm install", {
    cwd: "/workspace/vite-react",
    timeout: 120000,
  });

  // 3. Start the dev server (non-blocking)
  const server = await previewSandbox.startProcess(
    "npm run dev -- --port 8080",
    { cwd: "/workspace/vite-react" },
  );

  // Wait until the server is actually listening
  await server.waitForPort(8080, { timeout: 30000 });

  return Response.json({
    steps: {
      clone: {
        status: "success",
        message: alreadyCloned ? "Already cloned" : "Repository cloned",
      },
      install: {
        status: install.exitCode === 0 ? "success" : "failed",
        message:
          install.exitCode === 0 ? "Dependencies installed" : install.stderr,
      },
      server: {
        status: "success",
        message: "Dev server running",
        processId: server.id,
      },
    },
  });
}
bash
curl -X POST http://localhost:8787/preview
json
{
  "steps": {
    "clone": { "status": "success", "message": "Already cloned" },
    "install": { "status": "success", "message": "Dependencies installed" },
    "server": {
      "status": "success",
      "message": "Dev server running",
      "processId": "proc_abc123"
    }
  }
}

Bước 4: Expose port và proxy request Step 4: Expose Port and Proxy Requests ជំហាន 4: Expose port និង proxy request

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Your sandbox app accessible via your Worker's URL, with all requests proxied through.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
exposePort() and proxyToSandbox() turn your container port into a real URL users can visit.

Two things need to happen before the preview URL works:

Two things need to happen before the preview URL works:

Two things need to happen before the preview URL works:

First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:

First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:

First, update your Dockerfile to expose the port. This is required for local development so Docker maps the container port to your host:

dockerfile
FROM docker.io/cloudflare/sandbox:0.7.0

# Required: expose every port you plan to use
EXPOSE 8080

Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:

Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:

Second, update your Worker to handle proxying at the top of the fetch handler — before any other routes. Note that you also have to import proxyToSandbox from the SDK:

typescript
import { getSandbox, proxyToSandbox } from "@cloudflare/sandbox";

// Required: re-export the Sandbox Durable Object class
export { Sandbox } from "@cloudflare/sandbox";

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext,
  ): Promise<Response> {
    // Check for proxied requests first — must be before all other routes
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    const url = new URL(request.url);

    // ... rest of your routes
  },
};

Now add exposePort() as the final stage in the /preview endpoint:

Now add exposePort() as the final stage in the /preview endpoint:

Now add exposePort() as the final stage in the /preview endpoint:

typescript
if (url.pathname === "/preview" && request.method === "POST") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");

  // 1. Clone if needed
  const { exists: alreadyCloned } = await previewSandbox.exists(
    "/workspace/vite-react",
  );

  if (!alreadyCloned) {
    await previewSandbox.gitCheckout(
      "https://github.com/harshil1712/vite-react.git",
    );
  }

  // 2. Install dependencies
  const install = await previewSandbox.exec("npm install", {
    cwd: "/workspace/vite-react",
    timeout: 120000,
  });

  // 3. Start the dev server
  const server = await previewSandbox.startProcess(
    "npm run dev -- --port 8080",
    { cwd: "/workspace/vite-react" },
  );

  await server.waitForPort(8080, { timeout: 30000 });

  // 4. Expose the port — returns a URL users can visit
  const hostname = new URL(request.url).host; // For production, use hostname instead of host
  const exposed = await previewSandbox.exposePort(8080, { hostname });

  return Response.json({
    steps: {
      clone: {
        status: "success",
        message: alreadyCloned ? "Already cloned" : "Repository cloned",
      },
      install: {
        status: install.exitCode === 0 ? "success" : "failed",
        message:
          install.exitCode === 0 ? "Dependencies installed" : install.stderr,
      },
      server: {
        status: "success",
        message: "Dev server running",
        processId: server.id,
      },
      expose: {
        status: "success",
        message: "Port exposed",
      },
    },
    previewUrl: exposed.url,
  });
}

Test it:

Test it:

Test it:

bash
curl -X POST http://localhost:8787/preview
json
{
  "steps": {
    "clone": { "status": "success", "message": "Already cloned" },
    "install": { "status": "success", "message": "Dependencies installed" },
    "server": {
      "status": "success",
      "message": "Dev server running",
      "processId": "proc_abc123"
    },
    "expose": { "status": "success", "message": "Port exposed" }
  },
  "previewUrl": "http://localhost:8787/proxy/preview-env/http/8080/"
}

Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.

Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.

Open the previewUrl in your browser — you should see the Vite React app running live from inside the sandbox.

Bước 5: Stream log process Step 5: Stream Process Logs ជំហាន 5: Stream log process

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Real-time log output from your dev server, streamed to the client.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
Logs help you debug startup failures and monitor running processes — this is a genuinely separate endpoint since it returns a stream, not JSON.

Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:

Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:

Log streaming is a separate concern from the preview setup flow — it returns a long-lived SSE stream rather than a JSON response. Add a /preview/logs endpoint:

typescript
import { parseSSEStream, type LogEvent } from "@cloudflare/sandbox";

if (url.pathname === "/preview/logs") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");
  const processId = url.searchParams.get("pid");

  if (!processId) {
    return Response.json(
      { error: "pid query param required" },
      { status: 400 },
    );
  }

  const logStream = await previewSandbox.streamProcessLogs(processId);

  return new Response(logStream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
    },
  });
}

Test it using the processId from your /preview response:

Test it using the processId from your /preview response:

Test it using the processId from your /preview response:

bash
curl "http://localhost:8787/preview/logs?pid=proc_abc123"

You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:

You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:

You can also consume logs in the background using ctx.waitUntil — useful for logging server output without blocking the response:

typescript
// In your fetch handler signature: fetch(request, env, ctx)
ctx.waitUntil(
  (async () => {
    const logStream = await previewSandbox.streamProcessLogs(server.id);
    for await (const log of parseSSEStream<LogEvent>(logStream)) {
      console.log("[preview-server]", log.data);
    }
  })(),
);

Bước 6: Sẵn sàng production Step 6: Make It Production-Ready ជំហាន 6: រៀបចំឲ្យសម្រាប់ production

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A robust preview endpoint with error handling and process cleanup.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
Repeated calls shouldn't create duplicate processes, and failures should return clear error messages — not crash your Worker.

The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:

The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:

The current endpoint has two gaps: no error handling (a failed clone crashes the Worker) and no process cleanup (calling /preview twice starts duplicate dev servers). Fix both:

typescript
if (url.pathname === "/preview" && request.method === "POST") {
  const previewSandbox = getSandbox(env.Sandbox, "preview-env");
  const hostname = new URL(request.url).hostname;

  try {
    // 1. Clone if needed
    const { exists: alreadyCloned } = await previewSandbox.exists(
      "/workspace/vite-react",
    );

    if (!alreadyCloned) {
      await previewSandbox.gitCheckout(
        "https://github.com/harshil1712/vite-react.git",
      );
    }

    // 2. Install dependencies
    const install = await previewSandbox.exec("npm install", {
      cwd: "/workspace/vite-react",
      timeout: 120000,
    });

    // 3. Kill any existing dev server to avoid duplicates
    const processes = await previewSandbox.listProcesses();
    for (const proc of processes) {
      if (proc.command.includes("npm run dev")) {
        await previewSandbox.killProcess(proc.id);
      }
    }

    // 4. Start the dev server
    const server = await previewSandbox.startProcess(
      "npm run dev -- --port 8080",
      { cwd: "/workspace/vite-react" },
    );

    await server.waitForPort(8080, { timeout: 30000 });

    // 5. Expose the port
    const exposed = await previewSandbox.exposePort(8080, { hostname });

    return Response.json({
      steps: {
        clone: {
          status: "success",
          message: alreadyCloned ? "Already cloned" : "Repository cloned",
        },
        install: {
          status: install.exitCode === 0 ? "success" : "failed",
          message:
            install.exitCode === 0 ? "Dependencies installed" : install.stderr,
        },
        server: {
          status: "success",
          message: "Dev server running",
          processId: server.id,
        },
        expose: {
          status: "success",
          message: "Port exposed",
        },
      },
      previewUrl: exposed.url,
    });
  } catch (err) {
    return Response.json(
      { error: err instanceof Error ? err.message : "Preview setup failed" },
      { status: 500 },
    );
  }
}

This is the final version. Compared to Step 4’s code:

This is the final version. Compared to Step 4’s code:

This is the final version. Compared to Step 4’s code:

  • Process cleanup — listProcesses() and killProcess() stop any existing dev server before starting a new one, so repeated calls are safe
  • Error handling — try/catch wraps the entire flow, returning a clear 500 error instead of an unhandled exception
  • Same response shape — the steps object still reports per-step status, so debugging is straightforward

Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.

Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.

Live previews working! You can now clone, build, and serve full applications from inside a sandbox — all through a single API call. Next, you’ll learn the security patterns needed to make this production-ready.