# Next.js Hosting | Volcano

URL: https://volcano.dev/features/nextjs-hosting

> Deploy Next.js 15 and 16 to Volcano. Cloud builds on Node 22 or 24, a global CDN with HTTPS and IPv6, image optimization for next/image, monorepo builds with --app-root, and a failed build that never replaces the running site.

## Deploy Next.js straight from source

Volcano builds Next.js 15 and 16 on its own machines, serves the output from a global CDN over HTTPS, and never lets a failed build replace what is running.

```console
$ volcano cloud variables deploy
4 variables synced · NEXT_PUBLIC_* inlined at build time
$ volcano cloud frontends deploy --name web --path . --app-root apps/web --variable-scope all
✓ Frontend 'web' deployment started
$ volcano cloud frontends logs web --type build
next build · node 24 · pnpm 9 · 41 routes
$ volcano cloud frontends get web
Status  active   https://<frontend-id>.frontends.volcano.run
# the previous deployment served every request while this one built
```

## What deploying Next.js actually involves

Running next build takes one command on any machine. The work is elsewhere: pinning the Node major the build uses, packaging a pnpm workspace so shared packages still resolve, deciding which variables get inlined into the client bundle, and keeping a broken build from taking the live site with it.

Volcano runs the build in its own infrastructure. It installs with the package manager your package.json declares, or the one your lockfile belongs to, takes the Node major from package.json engines, and serves the output from a global CDN with HTTPS. Server routes run on demand. A failed build leaves the running deployment in place.

## The build runs on Volcano's machines

*Build*

You upload source, and Volcano installs from your lockfile with the package manager and Node major your package.json declares. There is no CI image to patch and no build box that drifts away from what production expects.

- Node 22 or 24, read from engines.node
- npm, pnpm or Yarn, at the version packageManager names
- No adapter, no plugin, no config to add

```console
packaging ~/acme            workspace root
app root  apps/web
runtime   node 24 · pnpm 9
next build · 41 routes · 12 prerendered
✓ artifact ready · promoting behind the CDN
```

## Cached at the edge, rendered on demand

*Delivery*

Build output and optimized images come from the CDN and never reach your code. Server components, route handlers and server actions run when a request needs them.

- next/image resized and served by Volcano
- No third-party image loader to configure
- HTTPS enforced, IPv6 enabled, TLS provisioned

Image: A Next.js storefront hosted on Volcano.

## A failed build never reaches production

*Releases*

The running deployment keeps answering while the next one builds, and a build that fails is recorded with its logs instead of shipped. Push again while a deployment is still waiting to build and the newer one takes its place.

- Every deployment keeps its status and its logs
- A newer push replaces one still waiting to build
- redeploy ships again from what you already uploaded

```console
d-43   active       next 16 · node 24 · 2m ago
d-42   failed       type error in apps/web/app/page.tsx
d-41   skipped      d-42 arrived before this one built
d-40   replaced     served every request until d-43 was ready
✓ traffic moves only when a build succeeds
```

## What every frontend gets

- **Next.js 15 and 16**: App Router, Pages Router, server components, route handlers, server actions and ISR, with no adapter.
- **Global CDN with TLS**: Output cached close to users, HTTPS enforced, IPv6 on, and a certificate provisioned for the hostname.
- **Image optimization**: next/image is resized and re-encoded by Volcano, so there is no loader to configure.
- **Monorepo builds**: Package the workspace root and build one member with --app-root, so shared packages resolve as they do locally.
- **Custom domains**: Attach as many custom domains to a frontend as your plan allows, with a certificate Volcano issues or one you supply.
- **Deployment history**: Every deployment keeps its status and build log, and redeploy ships the artifact you already uploaded.

## Deploy it, protect it, put it on your domain

### Deploy

```bash
# A new frontend gets only the project variables you select, for the build
# and the server runtime: all of them, or the names you list.
volcano cloud variables deploy

# Single app, with every project variable
volcano cloud frontends deploy --name web --path ./apps/web --variable-scope all

# Monorepo: package the workspace root, build one member, select by name
volcano cloud frontends deploy --name web --path . --app-root apps/web --variable-scope scoped \
  --variable NEXT_PUBLIC_VOLCANO_API_URL --variable NEXT_PUBLIC_VOLCANO_ANON_KEY

# NEXT_PUBLIC_* is inlined into the client bundle during the build, so
# changing one needs a redeploy. That is Next.js behaviour, not a Volcano one.
volcano cloud frontends redeploy web

volcano cloud frontends list
volcano cloud frontends get web
volcano cloud frontends logs web --type build
volcano cloud frontends logs web --type runtime --follow
```

### Middleware

```typescript
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createServerClient, withAuth } from '@volcano.dev/sdk/next/middleware';

const server = createServerClient({
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
});

export async function middleware(request: NextRequest) {
  const user = await withAuth(request, server);

  if (!user) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};
```

### Route handler

```typescript
// app/api/orders/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createServerClient, getTokenFromRequest } from '@volcano.dev/sdk/next/middleware';
import { VolcanoAuth } from '@volcano.dev/sdk';

export async function GET(request: NextRequest) {
  const token = getTokenFromRequest(request);
  if (!token) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }

  const server = createServerClient({
    anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
    apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL,
  });

  const { user, error } = await server.getUser(token);
  if (error || !user) {
    return NextResponse.json({ error: 'invalid session' }, { status: 401 });
  }

  const volcano = new VolcanoAuth({
    apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
    anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
    accessToken: token,
  });
  volcano.database('app');

  // No owner filter here. The row-level policy applies one for this token.
  const { data } = await volcano
    .from('orders')
    .select('id, total_cents, placed_at')
    .order('placed_at', { ascending: false })
    .limit(20);

  return NextResponse.json({ orders: data ?? [] });
}
```

### Domain

```bash
volcano cloud frontends domain create web \
  --domain app.acme.com \
  --cert cert.pem \
  --key key.pem \
  --chain chain.pem

# Point a CNAME at the target this prints.
volcano cloud frontends domain get web

volcano cloud frontends domain list
volcano cloud frontends domain delete web

# A frontend takes as many custom domains as your plan allows. The Volcano
# hostname keeps working alongside them, which is useful while DNS propagates.
```

## What people host on it

### Marketing and content sites

Prerendered pages cached at the edge, with incremental regeneration handled by your own app rather than a separate rendering service.

### Product dashboards

Middleware validates the session and redirects before a protected route renders. Route handlers read Postgres under that user's own policies.

### Storefronts

Volcano resizes the product imagery, inventory is read from Postgres at request time, and a price change is live without a rebuild.

## Works with the rest of Volcano

- [Data behind your routes](https://volcano.dev/features/postgres): Route handlers and server actions query the project's database with the same client the browser uses.
- [Protected routes](https://volcano.dev/features/authentication): Middleware helpers validate the session, so a protected page redirects before it renders.
- [User uploads](https://volcano.dev/features/file-storage): Upload from the browser straight to a policy-gated bucket, and serve public objects from a stable URL.
- [Long work in a function](https://volcano.dev/features/edge-functions): Move exports, imports and image pipelines out of the request into a function, or a durable function when they run long.

## Frequently asked questions

### Which frameworks can I deploy?

Next.js, and only Next.js: versions 15.x and 16.x. A standard project deploys without an adapter, a plugin or code changes.

### Which Node version and package manager does the build use?

Node 22 or 24, taken from engines.node in your package.json and defaulting to 22. The package manager and its version come from the packageManager field in package.json, or from your lockfile when that field is absent. npm, pnpm and Yarn are supported.

### How do I deploy from a monorepo?

Package the workspace root and name the member: volcano cloud frontends deploy --name web --path . --app-root apps/web. Workspace packages resolve during the cloud build the same way they do on your machine.

### Can I use my own domain?

Yes, on any plan. Attach as many custom domains to a frontend as your plan allows, each with a certificate Volcano issues or one you supply as PEM files, then point a CNAME at the target the CLI prints. A domain can also redirect to another one, such as example.com to www.example.com. The Volcano hostname keeps working alongside them, which helps while DNS propagates.

### What happens when a build fails?

The running deployment keeps serving and the failed build is recorded with its logs. If you start another deploy while one is already waiting, the newer one replaces it rather than both being built.

### How do environment variables reach the build?

From the project variables you select for the frontend: pass --variable-scope all on deploy, or --variable-scope scoped with a --variable for each name. Selected variables reach both the build and the server runtime. NEXT\_PUBLIC\_\* values are inlined into the client bundle while it builds, so changing one needs a redeploy — that is Next.js behaviour rather than a Volcano limitation.

## Get started

Ready to deploy your frontend? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
