Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.github
.vscode
.conductor
node_modules
.next
out
build
coverage
test-results
readme-assets
.env
.env.*
*.md
Dockerfile
.dockerignore
26 changes: 22 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
### Domain for the E2B cluster.
### Resolves infra-api (`https://api.<domain>`) and dashboard-api
### (`https://dashboard-api.<domain>`) unless overridden below.
NEXT_PUBLIC_E2B_DOMAIN=e2b.dev
PUBLIC_E2B_DOMAIN=e2b.dev
### Legacy fallback: NEXT_PUBLIC_E2B_DOMAIN (frozen at build time).

### =================================
### OPTIONAL ENVIRONMENT VARIABLES
Expand All @@ -16,13 +17,30 @@ NEXT_PUBLIC_E2B_DOMAIN=e2b.dev
### and sign-out is hidden.
# E2B_API_KEY=e2b_your_team_api_key

### Explicit API base URLs (override the NEXT_PUBLIC_E2B_DOMAIN resolution;
### Explicit API base URLs (override the cluster domain resolution;
### useful for local infra development).
# NEXT_PUBLIC_INFRA_API_URL=http://localhost:3000
# NEXT_PUBLIC_DASHBOARD_API_URL=http://localhost:3001

### Optional sandbox traffic base URL for local development proxies.
# NEXT_PUBLIC_E2B_SANDBOX_URL=http://sandbox.lvh.me:3002
### Runtime API base URLs. Unlike the NEXT_PUBLIC_ variables above, these are
### read when the server starts rather than baked into the build, so one
### prebuilt image can serve any install. They take precedence over the
### NEXT_PUBLIC_ overrides.
# E2B_INFRA_API_URL=http://127.0.0.1:3000
# E2B_DASHBOARD_API_URL=http://127.0.0.1:3010

### Base URL the BROWSER uses to reach sandboxes (terminal and filesystem
### inspector). Set this explicitly for local sandbox proxies. It must be
### reachable from both the browser and the server —
### the loopback below works only when the two are the same machine.
# PUBLIC_SANDBOX_URL=http://127.0.0.1:3002
### Legacy fallbacks, in order: E2B_SANDBOX_URL, NEXT_PUBLIC_E2B_SANDBOX_URL.
### With no sandbox URL, the SDK uses the cluster domain.
### Request headers never select this URL.

### Set to "false" when the dashboard is served over plain http (a LAN address
### or an IP), or the browser drops the api key cookie and the key form loops.
# DASHBOARD_COOKIE_SECURE=false

### OpenTelemetry (disabled unless the endpoint is set).
# OTEL_SERVICE_NAME=e2b-dashboard
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Nothing else in CI builds the image, so a break in the Docker build would
# otherwise go unnoticed until someone builds it by hand. This job runs on the
# files that can break it, not on every PR — a full Next build in Docker is
# minutes, and application changes are already covered by Test / Code Quality.
name: Container

on:
push:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- next.config.ts
- tsconfig.json
- package.json
- bun.lock
- scripts/check-app-env.ts
- scripts/container-smoke.sh
- src/lib/env.ts
- src/instrumentation.ts
- src/core/server/runtime-config.ts
- src/configs/cookies.ts
- .github/workflows/container.yml
pull_request:
branches: [main]
paths:
- Dockerfile
- .dockerignore
- next.config.ts
- tsconfig.json
- package.json
- bun.lock
- scripts/check-app-env.ts
- scripts/container-smoke.sh
- src/lib/env.ts
- src/instrumentation.ts
- src/core/server/runtime-config.ts
- src/configs/cookies.ts
- .github/workflows/container.yml
workflow_dispatch:

env:
FORCE_COLOR: "1"
CLICOLOR_FORCE: "1"

permissions:
contents: read

jobs:
smoke:
name: Build and Smoke-Test the Image
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Build the image and check the responses it serves
run: ./scripts/container-smoke.sh
61 changes: 61 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Three stages: Bun resolves the dependencies (bun.lock is the lockfile), Node
# runs the Next build, Node serves. The runtime stage carries only Next's
# standalone output, so the full dependency tree never ships in the image.
#
# The build runs under Node, not Bun: `bun run build` forks Next's page-data
# workers, and Bun's CommonJS interop throws "Expected CommonJS module to have
# a function wrapper" on the webpack output those workers load.
#
# The build fetches three Google Fonts families through next/font/google
# (src/app/fonts.ts): it needs outbound HTTPS to fonts.googleapis.com and
# fonts.gstatic.com, and fails there in an air-gapped environment.
FROM oven/bun:1.2.20 AS deps

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

FROM node:22-bookworm-slim AS builder

WORKDIR /app

# Only to run the prebuild env check, which is a TypeScript entrypoint.
COPY --from=deps /usr/local/bin/bun /usr/local/bin/bun
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Retain the legacy build argument as a fallback. PUBLIC_E2B_DOMAIN overrides
# it at runtime. The default resolves nowhere so an unconfigured container
# cannot accidentally reach someone else's deployment.
ARG NEXT_PUBLIC_E2B_DOMAIN=unset.invalid
ENV NEXT_PUBLIC_E2B_DOMAIN=${NEXT_PUBLIC_E2B_DOMAIN}
ENV NEXT_TELEMETRY_DISABLED=1

RUN bun scripts/check-app-env.ts
RUN node node_modules/next/dist/bin/next build --webpack

FROM node:22-bookworm-slim AS runtime

WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# server.js reads PORT (default 3000) and HOSTNAME (default 0.0.0.0). The
# default is 3001 so the dashboard does not land on 3000, which an E2B install
# already uses for its API when both share a host network.
ENV PORT=3001
ENV HOSTNAME=0.0.0.0

# Reported as service.version on OTEL traces (src/instrumentation.node.ts).
ARG BUILD=dev
ENV BUILD=${BUILD}

COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public

USER node
EXPOSE 3001

CMD ["node", "server.js"]
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,54 @@ Authentication is a single **team API key**:
- Visiting `/` shows a form to enter the key. It is validated against infra-api and stored in an httpOnly `e2b_api_key` cookie. All upstream calls happen server-side with the `X-API-Key` header — the key never reaches client JavaScript.
- Alternatively, set the `E2B_API_KEY` environment variable to pre-authenticate the whole deployment (single-user mode; the key form and sign-out are hidden).

### Configuration

| Variable | Read | Purpose |
|---|---|---|
| `PUBLIC_E2B_DOMAIN` | runtime | E2B cluster domain; used by the SDK and to derive `https://api.<domain>` and `https://dashboard-api.<domain>` |
| `PUBLIC_SANDBOX_URL` | per request | Optional sandbox traffic base URL, reachable from both the browser and server |
| `E2B_INFRA_API_URL` / `E2B_DASHBOARD_API_URL` | server start | Explicit server-side API URLs; override domain-derived URLs |
| `E2B_SANDBOX_URL` | per request | Legacy alias for `PUBLIC_SANDBOX_URL` |
| `NEXT_PUBLIC_E2B_DOMAIN` | build | Legacy fallback for `PUBLIC_E2B_DOMAIN` |
| `NEXT_PUBLIC_INFRA_API_URL` / `NEXT_PUBLIC_DASHBOARD_API_URL` | build | Legacy API overrides, below the corresponding `E2B_*` variables |
| `NEXT_PUBLIC_E2B_SANDBOX_URL` | build | Legacy sandbox URL, below both runtime names |
| `DASHBOARD_COOKIE_SECURE` | server start | `false` only for a plain-http install; the API key cookie then travels unencrypted. Defaults to secure in production builds |

Configure a prebuilt image with `PUBLIC_*` and `E2B_*` variables when starting
the container. Next does not give `PUBLIC_` any special behavior: the server
explicitly reads these values at runtime. `NEXT_PUBLIC_*` aliases remain
supported for existing builds, but their values are frozen by `next build`.
Restart the container and reload open pages after changing its configuration.

Resolution order (blank values are skipped):

- Domain: `PUBLIC_E2B_DOMAIN` → `NEXT_PUBLIC_E2B_DOMAIN`.
- Sandbox URL: `PUBLIC_SANDBOX_URL` → `E2B_SANDBOX_URL` → `NEXT_PUBLIC_E2B_SANDBOX_URL` → SDK domain routing.
- API URLs: corresponding `E2B_*` override → `NEXT_PUBLIC_*` override → URL derived from the resolved domain.

Every explicit URL must include `http://` or `https://`. Server initialization
validates the resolved API and sandbox URLs and `DASHBOARD_COOKIE_SECURE`,
even when telemetry is disabled. Invalid values stop startup and name the
variable. The cookie flag accepts `true` or `false` (case-insensitive, with
surrounding whitespace ignored); an empty value keeps the default.

The dashboard's Server Component layout resolves **only the domain and
sandbox URL** and passes them as props to a client `ClientConfigProvider`.
The terminal and filesystem inspector read this provider on their first
render, without a separate config request. API endpoints and team credentials
stay on the server. Both public settings are visible to browser users and
must contain no secrets.

For a local sandbox proxy, explicitly set `PUBLIC_SANDBOX_URL`, for example
`http://127.0.0.1:3002` when the browser and server run on the same machine.
Use an address reachable from both the browser and server. When a sandbox
URL is unset, the SDK uses domain-based routing, including when
`E2B_INFRA_API_URL` is set.

Server-side sandbox calls, such as terminal PTY cleanup, use the same domain
and sandbox URL resolution. `Host`, `X-Forwarded-Host`, and
`X-Forwarded-Proto` never determine sandbox destinations.

## Features

- **Sandboxes**: paginated live list, per-sandbox monitoring (CPU/memory/disk), logs, filesystem inspector, and an in-browser terminal
Expand Down Expand Up @@ -57,8 +105,8 @@ bun install
3. Set up environment variables
```bash
cp .env.example .env
# set NEXT_PUBLIC_E2B_DOMAIN (or explicit NEXT_PUBLIC_INFRA_API_URL /
# NEXT_PUBLIC_DASHBOARD_API_URL) to point at your infrastructure
# set PUBLIC_E2B_DOMAIN (and optionally E2B_INFRA_API_URL /
# E2B_DASHBOARD_API_URL) to point at your infrastructure
```

4. Start the development server
Expand All @@ -75,6 +123,36 @@ bun run build
bun run start
```

### Run it in a container

The repository builds a self-contained image: Bun resolves the dependencies,
Node runs the Next build, and Node serves the standalone output; the runtime
stage carries no dev dependencies.

```bash
docker build -t e2b-dashboard .
docker run --rm -p 3001:3001 \
-e PUBLIC_E2B_DOMAIN=your-domain.com \
e2b-dashboard
```

- `PORT` (default `3001`) and `HOSTNAME` (default `0.0.0.0`) are read by the
server at start. The default keeps the dashboard clear of port 3000, which
an E2B API already uses when both share a host network.
- `PUBLIC_E2B_DOMAIN` configures the cluster at container start, so the same
image can serve different installations. Use `PUBLIC_SANDBOX_URL` when the
default SDK routing does not fit your deployment.
- The legacy `NEXT_PUBLIC_E2B_DOMAIN` build argument is still supported. Its
default, `unset.invalid`, resolves nowhere so an unconfigured container
cannot accidentally talk to another deployment. Runtime configuration takes
precedence over that build-time fallback.
- The build needs outbound HTTPS for the three Google Fonts families in
`src/app/fonts.ts`; an air-gapped build fails there.
- `GET /api/health` reports dashboard-api's health and answers 503 while
dashboard-api is unreachable, so use `GET /` as the container liveness
check.
- `scripts/container-smoke.sh` builds the image and asserts those responses.

## Scripts

| Command | Description |
Expand Down
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const browserNodeModuleStubs = {
const config: NextConfig = {
reactStrictMode: true,
reactCompiler: true,
// Emits .next/standalone: a server plus only the traced dependencies, which
// is what the container image runs. `next start` still works from .next for
// local previews, and platform builds ignore this output.
output: 'standalone',
experimental: {
useCache: true,
turbopackFileSystemCacheForDev: true,
Expand Down
95 changes: 95 additions & 0 deletions scripts/container-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Builds the container image and checks the three responses a self-hosted
# install depends on. Needs Docker and outbound HTTPS: the Next build pulls
# the Google Fonts faces declared in src/app/fonts.ts.
set -euo pipefail

IMAGE="${IMAGE:-e2b-dashboard:smoke}"
PORT="${PORT:-3001}"
CONTAINER="e2b-dashboard-smoke-$$"
INVALID_CONTAINER="${CONTAINER}-invalid"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

cleanup() {
docker rm -f "${CONTAINER}" "${INVALID_CONTAINER}" >/dev/null 2>&1 || true
}
trap cleanup EXIT

echo "==> building ${IMAGE}"
docker build -t "${IMAGE}" "${ROOT}"

echo "==> starting ${CONTAINER} on port ${PORT}"
docker run -d --name "${CONTAINER}" -e PORT="${PORT}" -p "${PORT}:${PORT}" "${IMAGE}" >/dev/null

ready=0
for _ in $(seq 1 60); do
if curl -fs -o /dev/null "http://127.0.0.1:${PORT}/"; then
ready=1
break
fi
sleep 1
done

if [ "${ready}" != 1 ]; then
echo "FAIL: nothing answered on port ${PORT} within 60s" >&2
docker logs "${CONTAINER}" >&2 || true
exit 1
fi

fail=0
check() {
if [ "$3" = "$2" ]; then
echo "ok $1: $3"
else
echo "FAIL $1: expected $2, got $3" >&2
fail=1
fi
}

check "GET / serves the api key form" 200 \
"$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/")"

check "GET /sandboxes redirects to the key form" 307 \
"$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/sandboxes")"

check "GET /sandboxes redirect target" "http://127.0.0.1:${PORT}/?returnTo=%2Fsandboxes" \
"$(curl -sS -o /dev/null -w '%{redirect_url}' "http://127.0.0.1:${PORT}/sandboxes")"

# /api/health probes dashboard-api, which this run does not provide, so 503 is
# the correct answer here and proves route handlers are being served.
check "GET /api/health without a dashboard-api" 503 \
"$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/api/health")"

if [ "${fail}" != 0 ]; then
docker logs "${CONTAINER}" >&2 || true
exit 1
fi

check_invalid_config() {
local variable="$1" value="$2" status="" exit_code logs
docker run -d --name "${INVALID_CONTAINER}" --network none \
-e "${variable}=${value}" "${IMAGE}" >/dev/null

for _ in $(seq 1 50); do
status="$(docker inspect -f '{{.State.Status}}' "${INVALID_CONTAINER}")"
if [ "${status}" = "exited" ]; then break; fi
sleep 0.2
done

exit_code="$(docker inspect -f '{{.State.ExitCode}}' "${INVALID_CONTAINER}")"
logs="$(docker logs "${INVALID_CONTAINER}" 2>&1)"
if [ "${status}" != "exited" ] || [ "${exit_code}" = "0" ] || \
[[ "${logs}" != *"${variable}"* ]]; then
echo "FAIL: invalid ${variable} must stop startup and name the variable" >&2
echo "${logs}" >&2
exit 1
fi
echo "ok invalid ${variable} rejected at startup"
docker rm "${INVALID_CONTAINER}" >/dev/null
}

check_invalid_config PUBLIC_SANDBOX_URL missing-scheme.example:3002
check_invalid_config E2B_SANDBOX_URL ftp://sandbox.example
check_invalid_config DASHBOARD_COOKIE_SECURE off

echo "==> container smoke test passed"
Loading
Loading