> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blaxel.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up Expo with Blaxel Sandboxes

> Run an Expo React Native and web application in a Blaxel Sandbox and access it through secure preview URLs.

This guide explains how to run an Expo React Native and web application inside a Blaxel Sandbox, expose it securely using Blaxel previews, and access it on a mobile device.

## Prerequisites

* [Blaxel CLI](../../cli-reference/introduction) installed and authenticated with `bl login`
* Node.js 18 or later
* `@blaxel/core` installed in your project with `npm install @blaxel/core`

## 1. Understand the architecture

Running Expo inside a Blaxel Sandbox requires a few adjustments:

* Expo normally binds to local or internal URLs
* Blaxel exposes services through preview URLs
* Expo must use those preview URLs explicitly

Inject the preview URL into `app.json`, set `EXPO_PACKAGER_PROXY_URL`, restart the development server after configuration changes, and expose the Expo server through a [Blaxel preview URL](../../Sandboxes/Preview-url).

## 2. Build the sandbox image

Create a `Dockerfile`:

```dockerfile theme={null}
FROM node:22-alpine

RUN apk update && apk add --no-cache \
  git \
  curl \
  netcat-openbsd \
  && rm -rf /var/cache/apk/*

WORKDIR /app

COPY --from=ghcr.io/blaxel-ai/sandbox:latest /sandbox-api /usr/local/bin/sandbox-api

# Create an Expo project with the default template
RUN npx create-expo-app@latest .

# Install the dependencies required for Expo web support
RUN npx expo install react-dom react-native-web

# Pre-warm the Metro bundler cache using the development server
RUN timeout 120 npx expo start --web --port 8081 --no-dev-client 2>/dev/null || true

ENV PATH="/usr/local/bin:$PATH"

ENTRYPOINT ["/usr/local/bin/sandbox-api"]
```

## 3. Configure and deploy the image

Create `blaxel.toml` beside the `Dockerfile`:

```toml theme={null}
type = "sandbox"
name = "expo-template"

[runtime]
memory = 8192

[[runtime.ports]]
name = "expo-web"
target = 8081
```

Deploy the image:

```bash theme={null}
bl deploy
```

## 4. Create or reuse a sandbox

```typescript theme={null}
import { SandboxInstance } from "@blaxel/core";

const sandboxName = "my-expo-sandbox";

const sandbox = await SandboxInstance.createIfNotExists({
  name: sandboxName,
  labels: {
    framework: "expo",
  },
  image: "expo-template:latest",
  memory: 8192,
  ports: [
    { name: "preview", target: 8081, protocol: "HTTP" },
  ],
});
```

## 5. Configure preview response headers

Expo development servers require permissive CORS headers when accessed through a preview URL:

```typescript theme={null}
const responseHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
  "Access-Control-Allow-Headers":
    "Content-Type, Authorization, X-Requested-With, X-Blaxel-Workspace, X-Blaxel-Preview-Token, X-Blaxel-Authorization",
  "Access-Control-Allow-Credentials": "true",
  "Access-Control-Expose-Headers": "Content-Length, X-Request-Id",
  "Access-Control-Max-Age": "86400",
  Vary: "Origin",
};
```

Alternatively, use [custom domains](../../Infrastructure/Custom-domains) to expose previews on your own domain.

## 6. Create the preview and token

Expo runs on port `8081`. Create a private preview for that port:

```typescript theme={null}
const preview = await sandbox.previews.createIfNotExists({
  metadata: { name: "dev-server-preview" },
  spec: {
    responseHeaders,
    public: false,
    port: 8081,
  },
});
```

Generate a token that expires after one day:

```typescript theme={null}
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);
const token = await preview.tokens.create(expiresAt);
```

## 7. Inject the preview URL into Expo

Expo Router requires the correct origin when it runs behind a proxy. Update `app.json` in the sandbox:

```typescript theme={null}
async function addRouterOriginToAppJson(
  sandbox: SandboxInstance,
  previewUrl: string
) {
  const appJsonPath = "/app/app.json";
  const appJsonContent = await sandbox.fs.read(appJsonPath);
  const appJson = JSON.parse(appJsonContent);

  appJson.expo = {
    ...appJson.expo,
    extra: {
      ...(appJson.expo.extra || {}),
      router: {
        ...(appJson.expo.extra?.router || {}),
        origin: previewUrl,
      },
    },
  };

  await sandbox.fs.write(appJsonPath, JSON.stringify(appJson, null, 2));
}
```

## 8. Configure the proxy URL

Expo must serve assets through the preview URL. The following function updates `.env` only when the expected value is missing:

```typescript theme={null}
async function configureExpoProxyUrl(
  sandbox: SandboxInstance,
  previewUrl: string
): Promise<boolean> {
  const baseUrl = previewUrl.replace(/\/$/, "");

  let envContent = "";
  try {
    envContent = await sandbox.fs.read("/app/.env");
  } catch {
    // The file does not exist yet
  }

  const expectedEnvLine = `EXPO_PACKAGER_PROXY_URL=${baseUrl}`;

  if (envContent.includes(expectedEnvLine)) {
    console.log("Expo proxy URL already configured correctly");
    return false;
  }

  const lines = envContent
    .split("\n")
    .filter((line) => !line.startsWith("EXPO_PACKAGER_PROXY_URL="));
  lines.push(expectedEnvLine);

  await sandbox.fs.write("/app/.env", lines.join("\n"));
  console.log(`Configured Expo to use proxy URL: ${baseUrl}`);
  return true;
}
```

## 9. Start and monitor the development server

After setting the proxy URL, start the development server:

```typescript theme={null}
async function startDevServer(sandbox: SandboxInstance) {
  console.log("Starting dev server with updated configuration...");
  await sandbox.process.exec({
    name: "dev-server",
    command: "npx expo start --web --port 8081 --scheme exp",
    workingDir: "/app",
    waitForPorts: [8081],
    restartOnFailure: true,
    maxRestarts: 25,
  });
}
```

Stream its logs:

```typescript theme={null}
const logStream = sandbox.process.streamLogs("dev-server", {
  onLog(log) {
    console.log(log);
  },
});

logStream.close();
```

## 10. Access the Expo application

Open the web application at:

```text theme={null}
${preview.spec?.url}?bl_preview_token=${token.value}
```

To access the application on a mobile device, build an Expo URL from the authenticated preview URL:

```typescript theme={null}
const previewUrl = `${preview.spec?.url}?bl_preview_token=${token.value}`;
const expoUrl = previewUrl.replace("https://", "exp://");
```

Encode the `exp://` URL in a QR code:

```text theme={null}
exp://your-sandbox.preview.blaxel.ai/?bl_preview_token=your-token
```

Scan the QR code with your device's camera or the Expo Go application.

## 11. Run the complete example

```typescript theme={null}
import { SandboxInstance } from "@blaxel/core";

const sandboxName = "my-expo-sandbox";

const responseHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS, PATCH",
  "Access-Control-Allow-Headers":
    "Content-Type, Authorization, X-Requested-With, X-Blaxel-Workspace, X-Blaxel-Preview-Token, X-Blaxel-Authorization",
  "Access-Control-Allow-Credentials": "true",
  "Access-Control-Expose-Headers": "Content-Length, X-Request-Id",
  "Access-Control-Max-Age": "86400",
  Vary: "Origin",
};

async function addRouterOriginToAppJson(
  sandbox: SandboxInstance,
  previewUrl: string
) {
  const appJsonPath = "/app/app.json";
  const appJsonContent = await sandbox.fs.read(appJsonPath);
  const appJson = JSON.parse(appJsonContent);

  appJson.expo = {
    ...appJson.expo,
    extra: {
      ...(appJson.expo.extra || {}),
      router: {
        ...(appJson.expo.extra?.router || {}),
        origin: previewUrl,
      },
    },
  };

  await sandbox.fs.write(appJsonPath, JSON.stringify(appJson, null, 2));
}

async function configureExpoProxyUrl(
  sandbox: SandboxInstance,
  previewUrl: string
): Promise<boolean> {
  const baseUrl = previewUrl.replace(/\/$/, "");

  let envContent = "";
  try {
    envContent = await sandbox.fs.read("/app/.env");
  } catch {
    // The file does not exist
  }

  const expectedEnvLine = `EXPO_PACKAGER_PROXY_URL=${baseUrl}`;
  if (envContent.includes(expectedEnvLine)) {
    return false;
  }

  const lines = envContent
    .split("\n")
    .filter((line) => !line.startsWith("EXPO_PACKAGER_PROXY_URL="));
  lines.push(expectedEnvLine);

  await sandbox.fs.write("/app/.env", lines.join("\n"));
  return true;
}

async function startDevServer(sandbox: SandboxInstance) {
  await sandbox.process.exec({
    name: "dev-server",
    command: "npx expo start --web --port 8081 --scheme exp",
    workingDir: "/app",
    waitForPorts: [8081],
    restartOnFailure: true,
    maxRestarts: 25,
  });
}

async function configureExpo(
  sandbox: SandboxInstance,
  previewUrl: string
) {
  await addRouterOriginToAppJson(sandbox, previewUrl);
  await configureExpoProxyUrl(sandbox, previewUrl);
  await startDevServer(sandbox);
}

async function main() {
  try {
    const sandbox = await SandboxInstance.createIfNotExists({
      name: sandboxName,
      labels: {
        framework: "expo",
      },
      image: "expo-template:latest",
      memory: 8192,
      ports: [
        { name: "preview", target: 8081, protocol: "HTTP" },
      ],
    });

    const preview = await sandbox.previews.createIfNotExists({
      metadata: { name: "preview" },
      spec: {
        responseHeaders,
        public: false,
        port: 8081,
      },
    });

    const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);
    const token = await preview.tokens.create(expiresAt);

    await configureExpo(sandbox, preview.spec?.url!);

    const processes = await sandbox.process.list();
    if (!processes.find((process) => process.name === "dev-server")) {
      await startDevServer(sandbox);
    }

    const webUrl = `${preview.spec?.url}?bl_preview_token=${token.value}`;
    const expoUrl = webUrl.replace("https://", "exp://");
    console.log(`Web Preview URL: ${webUrl}`);
    console.log(`Expo Mobile URL: ${expoUrl}`);

    const logStream = sandbox.process.streamLogs("dev-server", {
      onLog(log) {
        console.log(log);
      },
    });

    process.on("SIGINT", () => {
      logStream.close();
      process.exit(0);
    });
  } catch (error) {
    console.error("Error:", error);
    process.exit(1);
  }
}

main();
```

This setup runs Expo fully inside a Blaxel Sandbox, securely exposes the development server, supports Expo Router and asset loading, provides mobile access through the `exp://` protocol, and starts the server after configuration changes. It is suitable for preview environments, internal demos, and AI-powered coding workflows.

## Resources

<CardGroup cols={2}>
  <Card title="Real-time previews" href="../../Sandboxes/Preview-url">
    Configure public and private preview URLs for sandbox services.
  </Card>

  <Card title="Sandbox images" href="../../Sandboxes/Templates">
    Build and manage reusable Blaxel Sandbox images.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.