Deploy a frontend
Build and deploy a Next.js site to Volcano with the CLI, set variables, attach a custom domain, and view logs.
Deploy a Next.js site to Volcano and serve it at a Volcano URL or your own domain. This uses the Volcano CLI; install it and sign in first (see the Quickstart).
1. Scaffold or bring your app
Start a new Next.js project scaffolded for Volcano, or use an existing one:
volcano init nextjs # new project
# or: cd into your existing Next.js appSelect the project you're deploying to:
volcano projects create my-app
volcano use my-app2. Set variables
Values your build needs go in project variables. Prefix anything the browser
needs with NEXT_PUBLIC_. Put the cloud project's values in
volcano/cloud.env; volcano/volcano.env holds the local development
server's:
NEXT_PUBLIC_API_URL=https://api.example.comThen save them to your cloud project:
volcano cloud variables deploy -f volcano/cloud.envWithout cloud, volcano variables deploy writes to local development
(volcano start), not to your cloud project.
3. Deploy
volcano cloud frontends deploy --variable-scope scoped --variable NEXT_PUBLIC_API_URLThe CLI uploads your app, Volcano builds it, publishes the assets, and returns
the site URL. New frontends use a scoped variable environment by default. Use
--variable-scope scoped and pass each required project variable with
--variable; variables not selected are not available to the build or runtime.
Scoped mode with no --variable flags exposes no project variables. Existing
frontends keep their current selection when these flags are omitted.
Watch progress and check status:
volcano cloud frontends list
volcano cloud frontends get my-siteRedeploy after changes — traffic stays on the live build until the new one is ready:
volcano cloud frontends redeploy my-siteFirst deploy with volcano-config.yaml
volcano cloud config deploy configures frontends but never creates one. An entry
for a frontend that does not exist yet is reported as skipped, and nothing
else happens for it. Create the frontend first, then apply the manifest:
volcano cloud frontends deploy --variable-scope scoped --variable NEXT_PUBLIC_API_URL
volcano cloud config deployPass the same variables on that first deploy that the manifest's frontend entry
selects. The manifest updates the selection afterwards, but a NEXT_PUBLIC_
value only reaches the browser bundle through a build, so a variable added by
cloud config deploy alone needs a redeploy before the browser sees it.
Deploy with the rest of the project
volcano cloud deploy deploys the frontends listed in
.volcano/source-layout.json together with the project's variables,
functions, and manifest. A frontend whose volcano-config.yaml entry sets
variable_scope: all, or variable_scope: scoped with a variables list,
builds with that selection, and the manifest is applied after the frontend
exists, so the two steps above happen in the right order:
{
"version": 1,
"frontends": [{ "name": "my-site", "workspace": "", "app_root": "" }]
}workspace is the directory to upload, relative to the project root, and
app_root is the Next.js app inside it. Projects created with
volcano init nextjs include this file. A connected Git repository
reads the same file. See the CLI deploy reference.
The deploy can't pass any other selection to a build. An entry with
variable_scope: shared, or one that leaves out either field, builds with the
selection the frontend already has, which is none for a new frontend. The CLI
warns, the manifest step applies the selection afterwards, and the next deploy
rebuilds with it. Until then, the browser bundle lacks those NEXT_PUBLIC_
values.
Deploy one build to several frontends
Serve one build from several frontends of a project, such as one per region or brand, without building it again for each. Deploy it to one frontend, then deploy that frontend's build to the others:
volcano cloud frontends deploy --name web --variable-scope scoped --variable NEXT_PUBLIC_API_URL
volcano cloud frontends deploy web-eu --from web
volcano cloud frontends deploy web-apac --from web--from web takes the deployment web serves. The target must already exist,
from a first deploy of its own. Through the API, send the source deployment's
ID to the target frontend:
curl -X POST "https://api.volcano.dev/projects/$PROJECT_ID/frontends/$FRONTEND_ID/deployments" \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"source_deployment_id":"3f6c2a1e-9b0d-4c51-8a39-0c6f2f1e8d42"}'The response is the target frontend, as for a redeploy: status: "provisioning",
or a pending_deployment_id while another of its deployments runs, with the
same latest-wins queueing. The new deployment records the build it published
in source_deployment_id.
The target gets its own copy of the source and the build. Volcano gives the
copy the target's deployment identity and new draft-mode keys, then publishes
it; nothing is compiled, so the deployment's build minutes cover publishing
alone. A later redeploy of the target compiles its copied source, and changes
to the source frontend never reach it. That includes the redeploy Volcano runs
when you add a region to the project; deploy the source's build again to share
it once more. The copy keeps the build's Next.js build
ID, so both frontends serve the same _next/static URLs, and it keeps the
key Next.js encrypts Server Action arguments with, since prerendered pages
embed values encrypted with it.
Next.js inlines build-time values into the build, so the variables the target would build with must equal the ones the source build read, by name and value. Volcano records a keyed digest of each value, unique to the build, and never stores the values. A difference is refused and names the variables:
{
"error": "build variables differ from the source build: NEXT_PUBLIC_API_URL",
"code": "build_variables_differ"
}Give the target the same variable selection, or redeploy the source if a value
changed since its build. The check runs again when the deployment starts. A
variable changed while it was queued fails the deployment without a retry: its
error_message starts with the refusal's code, such as
build_variables_differ:, and the target keeps serving what it served.
| Refusal | Status | Cause |
|---|---|---|
source_deployment_not_found | 404 | The deployment is not in this project. |
source_deployment_same_frontend | 400 | The deployment is the target's own; redeploy it instead. |
source_deployment_not_active | 409 | The deployment is not active or degraded. |
source_build_unavailable | 409 | Volcano no longer keeps the build, or the deployment predates kept builds. |
frontend_incompatible | 409 | The frontends use a different framework or app root. |
build_variables_differ | 409 | A build-time variable differs, as above. |
A project whose source is managed by Git deploys
from a push to its production branch, so deploying a build answers 409
without a code, as a direct deploy does.
Volcano keeps the build each frontend serves and removes it shortly after a
newer deployment of that frontend goes live. The target's copy is made when
the deployment is accepted, so a queued deployment still publishes after the
source frontend moves on. Deploy from a frontend's current deployment; one that was deployed before Volcano kept builds needs one
redeploy of the source first. Local development builds each frontend from its
own source and answers 501 with code: build_reuse_unavailable.
Deploy to local development
Leave out cloud to build and serve the frontend on the local development
server that volcano start runs:
volcano frontends deploy --name my-site --variable-scope scoped --variable NEXT_PUBLIC_API_URL
# or deploy it with the rest of the project
volcano deployThe local site is served at an address like
http://FRONTEND_ID.frontends.localhost:8080. volcano frontends get my-site
prints it once the first build finishes. Custom domains are cloud-only. To call
an HTTP-mode function from the local site, declare a
Frontend Function route with function_routes in
volcano-config.yaml; volcano deploy applies it after the frontend exists.
volcano frontends routes list|create|delete manages the same routes by hand.
What the CLI uploads
The CLI archives the project directory (--path, the current directory by
default) and skips:
node_modules,.next,dist, andbuild, at any depth.env.localand other.env*.localfilesvolcano/*.envandvolcano.env, which hold the project's variables, such asvolcano/cloud.env, and undervolcano cloud deploythe--env-fileit deployed, wherever it is.git, editor and OS files (.vscode,.idea,.DS_Store,*.swp), and*.log- anything matched by the root
.gitignore
When the app uses workspace: dependencies, the CLI archives the workspace root
instead of --path, so these rules, the root .gitignore, and the file limit
below apply to the whole workspace. Pass --app-root to archive only --path.
Symbolic links are skipped with a warning. More than 10,000 files fails the
deploy with too many files. .env and .env.production are uploaded, so
Next.js reads them during the build; keep secrets in project variables instead.
Build environment
| Tool | Version |
|---|---|
| Node.js | 22, or 24 when package.json engines.node asks for it |
| npm | The version bundled with that Node.js (npm 10 on Node.js 22), unless packageManager pins another; see Dependencies |
| Next.js | 15 or 16 |
Volcano runs your build script, or next build when there is none. On
Next.js 16, where Turbopack is the default, it adds --webpack to next build.
The server runtime uses the same Node.js major as the build, but its patch
version can differ. The build log prints the exact versions. See
supported frontend environments
for the engines.node rules.
Environment size
A frontend's server runtime environment is capped at 4096 bytes, the same limit
as a function's, counted as the UTF-8 bytes of every name and value. It holds
the selected variables other than NEXT_PUBLIC_* plus about 900 bytes of
values Volcano sets for the runtime, so plan for roughly 3,100 bytes of your
own. A variable write, scope change, or deploy that would exceed the limit is
rejected, and the running deployment keeps serving:
function environment exceeds the maximum size: frontend "web" environment is 5809 bytes (max 4096)The error response puts more context in front of this message. Select fewer
variables with --variable-scope scoped, or move large values out of the
runtime environment.
Dependencies
Volcano installs your dependencies before running the build, and picks the package manager in this order:
| Your project | Volcano installs with |
|---|---|
declares "packageManager": "pnpm@9.15.0" (or yarn@, npm@) in package.json | the manager you declared, at that version (for npm, see below) |
declares "packageManager" as anything else (bun@…, a bare pnpm) | the build fails |
| declares nothing and ships one lockfile | the manager that lockfile belongs to |
| declares nothing and ships several lockfiles | pnpm, then yarn, then npm |
| declares nothing and ships no lockfile | npm |
npm installs with the version bundled with your Node.js runtime unless
packageManager names a different npm major, so npm@11.2.0 on a runtime that
bundles npm 11.6.2 installs with 11.6.2.
packageManager wins over the lockfiles, so a stale pnpm-lock.yaml left in a
project that declares npm@10 no longer changes what runs. Declaring it is the
only way to be certain, and it is what to reach for when several lockfiles are
checked in — where a single manager is required, several lockfile families fail
the build rather than falling back to that order.
Ship the lockfile that belongs to that manager. Volcano installs from it, so
your build gets the versions you tested. Without it — none shipped, or
packageManager naming a manager whose lockfile is missing — dependencies
resolve fresh during the build and can differ from your local install. Where
locked installs are required, the build stops instead:
no pnpm lockfile found and ALLOW_UNLOCKED_INSTALLS=falseThe build log names the manager and version that were selected, so check there first when a build installed something you did not expect.
Regional runtime repair
After propagation retries, Volcano treats a regional runtime as missing only
when the frontend proxy receives a provider-confirmed not-found response for
the deployed runtime or its invocation URL. Volcano first verifies that the same
deployment is still serving and that the region is still expected, then queues durable repair
of that immutable generation. Repair does not rebuild source, create a new
deployment, or change what active and degraded mean.
Volcano does not repair application errors, ambiguous provider failures such as timeouts, throttling, or network errors, user deletion, regional convergence, or resources removed by a non-preserved staging purge. The request that detects drift can still fail while repair runs; Volcano does not replay it in another region. Repair is request-driven and does not proactively audit idle frontends.
Volcano controls the Next.js build ID, including when an application defines
generateBuildId. Redeploying the same frontend with identical source, build
variables, app path, architecture, and build toolchain keeps the same opaque
build ID and _next/static asset URLs. Changing any of those build inputs
changes the ID. Build IDs are scoped to the project and frontend, except that a
frontend deployed from another frontend's build
keeps that build's ID; do not use them as release identifiers.
Each generated frontend runtime image must fit within 4096 MB. Volcano checks an uncompressed upper bound—the exact pinned base-image layers plus the generated application layer—before publishing. A server or image-optimizer image over the limit fails the asynchronous deployment. Remove unused production dependencies or large generated files from the frontend bundle to reduce its size.
4. Add a custom domain
Custom domains are available on every plan and support Volcano-managed TLS or
bring-your-own-certificate (BYOC) TLS. For managed TLS, attach the domain with
--tls managed (Volcano CLI 0.50 or later):
volcano cloud frontends domain create my-site --domain app.example.com --tls managed
volcano cloud frontends domain get my-siteA volcano-config.yaml entry for this frontend that omits custom_domain
leaves the domain attached; custom_domain: null removes it. The manifest can
also attach it on its own:
version: 1
frontends:
- name: my-site
custom_domain:
domain: app.example.com
tls:
mode: managedIf your account already owns the domain, Volcano returns the certificate
validation CNAME right away. Otherwise it returns a _volcano TXT record; add
it within 72 hours to prove ownership, and Volcano then returns the CNAME. Add
the CNAME and keep it in DNS for renewal. Keep the TXT record too: the domain
moves to another account only once DNS stops serving it. If ownership
verification expires, Volcano removes the domain; once it is gone, submit it
again for a new challenge. Certificate
validation also expires after 72 hours.
If setup fails with failure_reason: certificate, check the validation CNAME,
delete the failed domain, wait for deletion to finish, and submit it again.
Volcano does not repeatedly request certificates for an abandoned setup.
An ownership failure means another account has already claimed the hostname;
retrying under the same account will not resolve that conflict.
If another account has only reserved the hostname, attaching the domain to a
frontend without one reports the _volcano TXT record your account must
publish. Add it and attach the domain again, with the CLI or a deploy, to take
over the reservation. A verified
domain moves only as described in
Move a domain to another account.
If a live domain's certificate becomes unavailable, the domain returns to
pending_verification, with verification_status: pending, while Volcano
re-validates it. Validation records can be
temporarily empty during this transition; keep polling and follow any records
returned. Temporary provider failures are retried automatically. When issuance
resumes after a failure, verification_status returns to pending. A terminal
certificate-validation failure requires the delete-and-create retry above.
To use BYOC instead, supply your own PEM certificate
and unencrypted private key (--chain is optional):
volcano cloud frontends domain create my-site \
--domain app.example.com \
--cert ./fullchain-leaf.pem \
--key ./privkey.pem \
--chain ./chain.pem
volcano cloud frontends domain get my-site # status + the default URL to point DNS atCheck with your DNS provider whether your domain is a zone apex; even a
delegated subdomain such as app.example.com can be one. If the provider
confirms it is not an apex, create a CNAME. For any apex, use a
provider-supported ALIAS, ANAME, or CNAME-flattening record. Point the record
at the default Volcano frontend hostname
(<frontend-id>.frontends.<region-domain>, from domain get / frontends get).
That host is stable across redeploys. For a provider-confirmed non-apex:
app.example.com. CNAME <frontend-id>.frontends.volcano.run.The domain goes active once Volcano finishes attaching the certificate —
that status doesn't confirm routing DNS is live, so verify it separately. See
custom domains for lifecycle and BYOC rotation,
and Wait out the switchover before
treating an error after active as a failure.
To serve the site on more hostnames, or send example.com to
www.example.com, list every domain under custom_domains instead of
custom_domain. Point every domain's DNS at the frontend. Once
www.example.com is active, give the apex a redirect and deploy again:
version: 1
frontends:
- name: my-site
custom_domains:
- domain: www.example.com
tls:
mode: managed
- domain: example.com
tls:
mode: managed
redirect:
to: www.example.comA request for https://example.com/docs?page=2 gets a 308 to
https://www.example.com/docs?page=2 without reaching your app. SUPERAGENT
allows 10 domains per frontend, HOBBY one. See
Redirect one domain to another.
5. View logs
volcano cloud frontends logs my-siteBuild logs show the selected Node.js and package-manager versions, dependency
installation, Next.js detection, the build command, and your Next.js build
output. Validation failures include an actionable message. If Volcano cannot
prepare prerendered pages, the build log names the installed Next.js version.
For an unfamiliar cache format, it recommends a supported version for that major
when it differs from the installed version. If the log recommends a version,
pin next to it, update your lockfile, and rebuild, or contact support.
For other packaging failures, or failures on the recommended version, contact support.
Detailed platform setup, packaging, and publishing diagnostics stay in internal
operator logs.
Next
- Frontends overview — how builds, variables, and domains fit together.
- Environment variables — build vs runtime.
- Frontend API reference — deploy over HTTP.
- Deploy from GitHub — redeploy on every push instead of running the CLI.
Next.js configuration support
Volcano applies the routing in your next.config file the way next start does:
redirects()rewrites(), includingbeforeFiles,afterFiles, andfallbackheaders()hasandmissingconditions onhost,header,cookie, andquery- Middleware, in
proxy.js(Next.js 16) ormiddleware.js
For example, send visitors from your apex domain to www:
// next.config.mjs
export default {
async redirects() {
return [
{
source: '/:path*',
has: [{ type: 'host', value: 'example.com' }],
destination: 'https://www.example.com/:path*',
permanent: true,
},
];
},
};curl -sI 'https://example.com/pricing?plan=pro' | grep -iE '^(HTTP|location)'
# HTTP/2 308
# location: https://www.example.com/pricing?plan=proRequests to www.example.com and to your frontend's default URL are not redirected. Conditions match the way Next.js matches them:
- A
hostcondition sees the hostname the visitor requested, lowercased and without its port, so write itsvaluein lowercase. - A
valueis a regular expression that must match the whole value, soexample.comdoes not matchwww.example.com. - A condition without a
valuerequires only that the header, cookie, or query parameter is present with a value;?draftand?draft=do not satisfy it. Its value becomes a parameter named after the key's letters, lowercased for a header, soX-Tenantgives:xtenant. - Named groups in a
value, such as(?<tenant>[^.]+)\\.example\\.com, become parameters you can use indestinationand in the keys and values ofheaders. - In a
destinationpath or host, a parameter from a condition is percent-encoded, so a/,?, or#in the request's value stays inside its segment and cannot send the visitor to another host. Next.js inserts it as sent, so/docs/:path*with?path=guides/setupreaches/docs/guides%2Fsetuphere rather than/docs/guides/setup; capture the segments fromsourceinstead.
Redeploy existing frontends to apply this matching. A rule that relied on a partial match stops matching after the redeploy: a user-agent value of Googlebot needs .*Googlebot.*. Next.js matches conditions the same way, so apart from the encoding above, configuration written for Next.js is unaffected.
Known limits:
- A request for a file in
public/is answered with the file.redirects,headers, and middleware do not run for it. - The CDN caches a page by its URL, host, and a few request headers such as
Authorization, but not by cookies or other headers. When acookieorheadercondition sends visitors to different content at the same URL, keep that route out of the shared cache, for example by reading the cookie in the page. See Caching. - Middleware
matcherentries match onsourceonly. Theirhasandmissingconditions are not evaluated, so check the condition inside the middleware. - Rewrites to files in
public/do not resolve whenbasePathand i18n are combined. See Rewrite routes to public files.
Optimize images under a basePath
next/image works in an app with a basePath. Its requests go to <basePath>/_next/image, which Volcano optimizes and caches like /_next/image at the root. Only the app's own basePath is optimized: another <prefix>/_next/image reaches your app like any other route, so a rewrite can still send it to another Next.js app. As Next.js requires, include the basePath in a src that names a file in public/:
// next.config.mjs sets basePath: '/docs'
import Image from 'next/image';
import hero from './hero.png';
export default function Page() {
return (
<>
<Image src="/docs/logo.png" alt="Logo" width={64} height={64} />
<Image src={hero} alt="Hero" />
</>
);
}Redeploy existing frontends to optimize images under their basePath.
Rewrite routes to public files
With the default Volcano frontend configuration, beforeFiles rewrites can serve files from your app's public directory:
// next.config.mjs
export default {
async rewrites() {
return {
beforeFiles: [{ source: '/dashboard/:path*', destination: '/app/index.html' }],
afterFiles: [],
fallback: [],
};
},
};Place the document at public/app/index.html. Requests to /dashboard and its nested paths return that document while preserving the browser URL. Headers configured for the requested route still apply. Missing files remain 404 responses.
Rebuild existing deployments to enable public-file rewrites.
- Use regular files in
publicfor rewrite destinations. Symbolic links are not included. - Files reachable through rewrites increase deployment size. Unrelated public files and identity self-rewrites are excluded from the server copy.
- The server copy is capped at 256 MiB and 20,000 files, and each file is capped at 199 MiB so it fits in a single streamed response. The build warning names files skipped at either byte cap. A rewrite to a skipped file returns 404; narrow the rewrite destinations or change the app to use the file's direct static URL.
- Rewrites to public files do not support byte ranges. Use direct static URLs for seekable media and resumable downloads.
- Configured locales work when the app has no
basePath, andbasePathworks when i18n is disabled. The build warns that rewrites to public files do not resolve whenbasePathand i18n are combined.
Public-file responses include a content-based ETag. Uncompressed GET responses include a byte length. Matching conditional requests return 304 without reading the file body.
Precompressed public files such as .gz downloads retain their compressed bytes and are served as downloads. Dynamic rewrite parameters can include encoded slashes (%2F) to reach nested public files.
The default max-age=0 still requires revalidation; validators reduce transferred bytes, not the number of origin requests.