Setting up Next.js with Blaxel Sandboxes
Last updated: February 26, 2026
This guide explains how to run a Next.js application inside a Blaxel Sandbox and expose it securely using Blaxel Previews.
Overview
The guide covers:
Building a sandbox image for Next.js
Creating or reusing a Blaxel sandbox
Configuring Next.js to work behind Blaxel previews
Launching Next.js and accessing it through a preview URL
Prerequisites
Before starting, ensure you have:
Bl
axel CLI installed and authenticated (
bl login)Node.js 18+ installed
@blaxel/corepackage installed in your project (npm install @blaxel/core)
Architecture Overview
Running Next.js inside a Blaxel sandbox requires minimal adjustments compared to local development:
Next.js binds to all interfaces by default in development mode
Blaxel exposes services via preview URLs
The preview URL handles routing and authentication
This is solved by:
Running the Next.js dev server on port 3000
Exposing the Next.js dev server via a Blaxel Preview
Sandbox Image for Next.js
Dockerfile
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 Next.js project with TypeScript, Tailwind, App Router, and Turbopack
RUN mkdir -p /app \
&& npx create-next-app@latest /app --use-npm --typescript --eslint --tailwind --src-dir --app --import-alias "@/*" --no-git --yes \
&& cd /app && npm install --save @next/swc-linux-x64-musl
COPY ./next.config.ts /app/next.config.ts
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Add npm global modules to PATH
ENV PATH="/usr/local/bin:$PATH"
ENTRYPOINT ["/entrypoint.sh"]entrypoint.sh
Create an entrypoint script that starts the sandbox API and the dev server:
#!/bin/sh
# Set environment variables
export PATH="/usr/local/bin:$PATH"
# Start sandbox-api in the background
/usr/local/bin/sandbox-api &
# Function to wait for port to be available
wait_for_port() {
local port=$1
local timeout=30
local count=0
echo "Waiting for port $port to be available..."
while ! nc -z localhost $port; do
sleep 1
count=$((count + 1))
if [ $count -gt $timeout ]; then
echo "Timeout waiting for port $port"
exit 1
fi
done
echo "Port $port is now available"
}
# Wait for port 8080 to be available
wait_for_port 8080
# Execute curl command to start Next.js dev server
echo "Running Next.js dev server..."
curl http://localhost:8080/process \
-X POST \
-H "Content-Type: application/json" \
-d '{
"name": "dev-server",
"workingDir": "/app",
"command": "npm run dev -- --port 3000",
"waitForCompletion": false,
"restartOnFailure": true,
"maxRestarts": 25
}'
waitnext.config.ts
Create a next.config.ts file to configure Next.js for use with Blaxel previews:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
allowedDevOrigins: ["*.preview.bl.run"],
};
export default nextConfig;The allowedDevOrigins: ["*.preview.bl.run"] setting allows the dev server to accept requests from the Blaxel preview origin, which is required for Blaxel preview URLs to work correctly. If you have configured a custom domain in your Blaxel workspace, you should also add it to this array (e.g., ["*.preview.bl.run", "*.preview.mycompany.com"]).
blaxel.toml
Create a blaxel.toml file in the same directory as your Dockerfile:
type = "sandbox"
name = "nextjs-template"
[runtime]
memory = 4096
[[runtime.ports]]
name = "nextjs-dev"
target = 3000
protocol = "tcp"Deploying the Image
Deploy the image by running:
bl deployCreating or Reusing a Sandbox
import { SandboxInstance } from "@blaxel/core";
const sandboxName = "my-nextjs-sandbox";
const sandbox = await SandboxInstance.createIfNotExists({
name: sandboxName,
labels: {
framework: "nextjs",
},
image: "nextjs-template:latest",
memory: 4096,
ports: [
{ name: "preview", target: 3000, protocol: "HTTP" },
],
});Configuring CORS for Next.js Preview
Next.js dev servers work well with permissive CORS headers when accessed through a preview URL:
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, you can use custom domains to expose previews on your own domain.
Creating the Blaxel Preview
Next.js runs on port 3000, so we expose that port via a preview:
const preview = await sandbox.previews.createIfNotExists({
metadata: { name: "dev-server-preview" },
spec: {
responseHeaders,
public: false,
port: 3000,
},
});Generating a Preview Token
To securely access the preview, a token is required:
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24); // 1 day
const token = await preview.tokens.create(expiresAt);Starting the Dev Server
If not using the entrypoint script, you can start the dev server programmatically:
async function startDevServer(sandbox: SandboxInstance) {
console.log("Starting Next.js dev server...");
await sandbox.process.exec({
name: "dev-server",
command: "npm run dev -- --port 3000",
workingDir: "/app",
waitForPorts: [3000],
restartOnFailure: true,
maxRestarts: 25,
});
}Streaming Next.js Logs
To monitor the Next.js dev server output in real-time:
const logStream = sandbox.process.streamLogs("dev-server", {
onLog(log) {
console.log(log);
},
});
// When done monitoring, close the stream:
logStream.close();Accessing the Next.js App
Once everything is running, the app is available at:
${preview.spec?.url}?bl_preview_token=${token.value}Complete Example
Here is a full runnable example combining all the steps:
import { SandboxInstance } from "@blaxel/core";
const sandboxName = "my-nextjs-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 startDevServer(sandbox: SandboxInstance) {
await sandbox.process.exec({
name: "dev-server",
command: "npm run dev -- --port 3000",
workingDir: "/app",
waitForPorts: [3000],
restartOnFailure: true,
maxRestarts: 25,
});
}
async function main() {
try {
// Create or reuse the sandbox
const sandbox = await SandboxInstance.createIfNotExists({
name: sandboxName,
labels: {
framework: "nextjs",
},
image: "nextjs-template:latest",
memory: 4096,
ports: [
{ name: "preview", target: 3000, protocol: "HTTP" },
]
});
// Create preview
const preview = await sandbox.previews.createIfNotExists({
metadata: { name: "preview" },
spec: {
responseHeaders,
public: false,
port: 3000,
},
});
// Generate preview token
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24);
const token = await preview.tokens.create(expiresAt);
// Start dev server if not already running
const processes = await sandbox.process.list();
if (!processes.find((p) => p.name === "dev-server")) {
await startDevServer(sandbox);
}
// Print access URL
const webUrl = `${preview.spec?.url}?bl_preview_token=${token.value}`;
console.log(`Next.js Preview URL: ${webUrl}`);
// Stream logs
const logStream = sandbox.process.streamLogs("dev-server", {
onLog(log) {
console.log(log)
},
});
// Keep running until interrupted
process.on("SIGINT", () => {
logStream.close();
process.exit(0);
});
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
main();Next.js Specific Features
Turbopack
The template uses Turbopack, Next.js’s Rust-based bundler, for significantly faster development builds. Turbopack provides:
Faster cold starts
Instant hot module replacement (HMR)
Optimized incremental compilation
The @next/swc-linux-x64-musl package is pre-installed for optimal performance on Alpine Linux.
App Router
The template comes pre-configured with the App Router (/app directory structure). This provides:
Server Components by default
Nested layouts
Loading and error states
Server Actions
TypeScript
Full TypeScript support is enabled out of the box with strict type checking.
Tailwind CSS
Tailwind CSS is pre-configured for styling. The src/app/globals.css file includes the Tailwind directives.
ESLint
ESLint is configured with Next.js recommended rules for code quality.
Summary
This setup allows you to:
Run Next.js fully inside Blaxel sandboxes
Securely expose the dev server using Blaxel previews
Support Fast Refresh correctly through the preview URL
Leverage Next.js App Router with Server Components
Use Turbopack for fast development builds
Use TypeScript, Tailwind CSS, and ESLint out of the box
This approach is ideal for preview environments, internal demos, and AI-powered coding workflows built on Blaxel.