Frontends overview

Deploy static and server-rendered sites on Volcano, with build/runtime variables, custom domains, and zero-downtime redeploys.

A frontend is a web site — static or server-rendered — that Volcano builds, hosts, and serves. You push your project's source; Volcano builds it, publishes the static assets to the edge, and runs any server-rendered routes on managed runtimes. Every frontend belongs to a project.

How it works

  1. You deploy your source (the CLI uploads a tar.gz of your app).
  2. Volcano builds it, resolving your project variables at build time.
  3. Static assets publish to the edge; server-rendered routes run on managed runtimes.
  4. The site is served at a Volcano URL, or at your own custom domain.

Redeploys are zero-downtime: the live build keeps serving until the new one is ready, then traffic cuts over. A failed redeploy leaves the previous deployment running. Deploys use latest-wins queueing — a newer deploy supersedes an older queued one.

Frameworks

Next.js is supported today (framework: nextjs), including static export and server-side rendering. For a monorepo, point Volcano at the app with app_root (for example apps/web).

For workspace projects, Volcano uses the workspace install root consistently when tracing and packaging server dependencies. An additional lockfile inside the app directory does not change the packaged application's root.

Default browser icon

Sites on volcano.run and its subdomains use the Volcano browser-tab icon when the app returns 404 for /favicon.ico. Existing app icons take precedence. To use your own icon in Next.js, add app/favicon.ico or configure an icon in your page metadata. Custom domains outside volcano.run keep the app's original response.

Compression

Responses are compressed automatically — no plugin or middleware needed. Send an Accept-Encoding header and you get the best encoding you accept:

curl -sI -H 'Accept-Encoding: br, gzip' https://your-site.frontends.volcano.run/ | grep -i content-encoding
# content-encoding: br

Static assets are compressed at the edge with Brotli or gzip. Server-rendered pages and API routes are compressed by the runtime, which also supports deflate. Both prefer Brotli, and both honour quality values: a coding you refuse with q=0 is never sent, and a response you accept no coding for arrives uncompressed.

A route that sets its own Content-Encoding keeps it. Those bytes are shipped exactly as the route wrote them, never compressed a second time:

// pages/api/report.js
export default function handler(req, res) {
  res.setHeader('Content-Encoding', 'gzip');
  res.status(200).send(gzipSync(Buffer.from(report)));
}

Compressed request bodies are passed through untouched too, so a route that accepts Content-Encoding: gzip reads the compressed bytes and decodes them itself.

Caching

Volcano serves your site through a CDN. Static assets, optimized images, and pages Next.js built ahead of time are held at the edge, so repeat visitors are answered without reaching your runtime. Check any response with X-Cache:

curl -sI https://your-site.frontends.volcano.run/pricing | grep -i -e x-cache -e cache-control
# cache-control: s-maxage=31536000
# x-cache: Hit from volcano

Every response also says which build and region answered. Your app's own headers are served as written, and Volcano's internal ones are never returned:

curl -sI https://your-site.frontends.volcano.run/pricing | grep -i x-volcano
# x-volcano-version: staging-abc1234
# x-volcano-region: us-east-1

Your app decides what is shared. The edge stores nothing that does not ask to be stored, so Next.js's own rendering choice is the whole contract:

RouteWhat Next.js sendsAt the edge
Prerendered at builds-maxage=31536000Cached until something replaces it
export const revalidate = 60s-maxage=60Cached for 60 seconds
export const dynamic = 'force-dynamic'no-storeNever cached
Reads cookies, headers, or search paramsno-storeNever cached

So a page that is always a Miss is a page your app is rendering per request. If you expected it to be cached, make it static — the usual cause is a dynamic API called in the page or in a layout above it, which opts the whole route out.

A cached page is kept for the lifetime it asked for. A page prerendered at build stays at the edge until something replaces it, so a visitor returning the next day is answered without your runtime being involved at all. A page with export const revalidate = N expires on its own after N seconds.

Three things replace a page before then: a redeploy, revalidatePath(), and revalidateTag(). All of them clear only the pages they affect — publishing one post does not empty the cache for the rest of your site:

// app/api/publish/route.js
import { revalidatePath } from 'next/cache';

export async function POST() {
  revalidatePath('/blog'); // /blog is refetched on the next visit; nothing else moves
  return Response.json({ revalidated: true });
}

Queued cache regeneration from an older deployment is retired after a redeploy. The redeploy moves that frontend to a fresh set of edge pages, so the old ones are no longer served. If that edge update did not complete, Volcano clears each affected page before retiring its queued work. Only the current build can regenerate its cache. Revalidation for other frontends is unaffected.

Two limits come with revalidatePath(). It clears the exact address that was revalidated — both /blog and /blog/, so a site that canonicalizes the trailing slash still drops the stored page — and the cache treats each query string as its own entry, so clearing /blog leaves /blog?utm_source=newsletter serving until that entry's own lifetime runs out. Redeploy when you need every variant of a page gone at once. A rewritten route is cleared at the address the visitor requested, not the destination the rewrite resolved. And the route has to be one Volcano can name: a path ending in * is rejected, because there is no way to clear that one page without also clearing every page sharing its prefix. An asterisk anywhere else in a path is fine, and so is a path of up to about 3,950 characters. A path Volcano cannot clear is refused and recorded in your function's logs; revalidatePath() itself still returns normally, so check the logs if a page you cleared is still serving the old content.

Passing "layout" as the second argument marks the layout and everything beneath it, which is more pages than a single address can name. Volcano clears the layout's own page. For the pages under it, tag their data and use revalidateTag(), or redeploy — a prerendered page is otherwise kept until something replaces it.

The same limit applies to a typed dynamic pattern such as revalidatePath("/product/[slug]", "page"): that names /product/[slug], not /product/123 or /product/456. Tag the data those pages are built from if you need them cleared by name.

revalidateTag() clears pages too. A tag names data rather than an address, so the CDN keeps the tags a page was built from alongside it and clears every page carrying the tag when you mark it — wherever those pages live:

// app/api/publish/route.js
import { revalidateTag } from 'next/cache';

export async function POST() {
  revalidateTag('posts'); // every page built from `posts` is cleared; nothing else moves
  return Response.json({ revalidated: true });
}

Both reach visitors within seconds of the call, and a tag costs the same whether it clears two pages or ten thousand. A page is only cleared this way if it was stored after being built from that tag, so the first build of a page following a tag change is what puts it under the tag.

Unlike revalidatePath(), a tag is not tied to an address, so it clears every query-string variant of the pages it covers.

A page carries up to about forty tags to the CDN, and each tag has to be under about 215 characters with no spaces. Tags past either limit still work inside your app — they just will not clear the page from the CDN, so keep the tags you publish with short and few.

Signed-in pages are not shared. The CDN keys on Authorization but not on cookies, so an answer that depends on who is asking is held out of the shared cache rather than handed to the next visitor. Five things take a response out of it, whatever its Cache-Control says:

  • it sets a cookie;
  • it answers a request that carried an Authorization header;
  • its Vary names a header the CDN does not key on. The key holds the compression headers, the App Router's own routing headers, Authorization, and the host the request was made to, so Vary on any of those is honoured while Vary: Cookie or Vary: Accept-Language takes the response out;
  • its Content-Security-Policy carries a nonce-, which authorizes the inline scripts of one response and must not be replayed to anyone else;
  • its route is matched by middleware (see below).

A page that reads the session cookie is already rendered per request, so it is never stored to begin with. The one way to defeat all of this is to set Cache-Control: public or s-maxage yourself on a page whose content depends on who is asking — that is your app telling the CDN the opposite, for a request it cannot tell apart from anyone else's.

Most of these rules act on a response, not on the route that produced it. Authorization is in the cache key, so a request carrying a token never reads an answer stored for an anonymous one. Cookies are not, and that is the gap to design around: ask the same URL without the session cookie and the answer is stored under whatever your app declared, and a later request that does carry one can be served that copy for as long as your app said the page could live. On a route whose answer depends on a cookie, do not declare it shareable.

A prefetch and a full page load of the same URL are cached separately, as are the same page requested on two different domains.

Middleware turns caching off for the routes it matches. This covers proxy.js (Next.js 16) and middleware.js, on the Edge or Node.js runtime. Middleware runs in your runtime, so a page answered from the edge would skip it — a route gated on a cookie would be served to the next visitor without one. Rather than guess which middleware is a security check, Volcano keeps every matched route out of the shared cache:

// proxy.js — /dashboard is never cached, everything else still is
export const config = { matcher: ['/dashboard/:path*'] };

Narrow the matcher to the routes that need it. Middleware declared without a matcher runs on every request, which leaves nothing cacheable. If you use middleware only to rewrite or add headers, moving that logic into next.config.js rewrites or into the page itself keeps the route at the edge.

Redeploying retires everything your frontend has at the edge — pages and optimized images alike. Each deployment is cached under its own key, so the new build starts with an empty edge cache and nothing has to be cleared before it goes live; the old build's copies are cleared in the background. Once the deploy reports the new build active, requests move to it, usually within ten seconds and at most about a minute. Until they do, and while the deploy is still rolling out, a request can get either build; after that, no page or optimized image comes from the old build. Redeploying cannot clear a browser's copy, which is why Next.js puts a build hash in the filename of every asset it expects to change.

Files under public/ are served as files. They keep their browser cache metadata, and they do not run middleware or authentication, so do not put protected content there. Naming one so it looks like a build asset does not make it one: only the hashed files your build actually emitted are treated as content-addressed.

/_volcano/static and /_volcano/immutable are reserved for Volcano's own cache routing. A request a visitor makes under either prefix returns 404 Not Found, so do not put your own routes or files there. Your assets are served from their own paths and only pass through these namespaces internally.

Local mode puts no CDN in front of your app — every request reaches it — so X-Cache and everything above only applies once deployed. A page that is cached wrongly will look fine locally.

Response time on the first request

A request your runtime has to answer needs that runtime started, and starting it takes time if nothing has run there recently. That startup is what makes an occasional request slower than the ones after it.

Volcano keeps your runtimes started for you, in every region the frontend is deployed to, so most visitors never pay for it:

  • For two days after each deploy, whether or not anyone visits. You deploy, then open your own site — that first look is the one worth being fast, and it is fast without any traffic having warmed it up.
  • For a day after the most recent request, for as long as your site keeps being used. A site with a visitor every few hours stays continuously ready.

A site that goes a full day without a single request stops being kept ready, and its next visitor waits for a runtime to start. That request also puts the site back on the list, within a couple of minutes, so a site coming back to life is slow once rather than slow repeatedly. Redeploying restores the two-day window immediately.

Nothing here is a setting. Both windows are the same on both plans; SUPERAGENT keeps two runtimes ready per region instead of one, so two visitors arriving at once are both answered without a startup. Keeping runtimes ready is not billed as requests or bandwidth, and it does not appear in your usage.

Keeping a frontend ready renders a page, so anything your server-side code logs while doing that appears in your logs alongside real visits. Only the logging is shared; those renders are still not counted as requests.

Measuring where a response spent its time

Every response Volcano proxies from your site carries:

X-Volcano-Origin-Ms: 42

That is how long your site took to start answering, measured from the moment Volcano asked it. A page streams to the browser as it is produced, so by the time the last byte leaves there is nowhere left to write a header — first byte is the part that can be reported. Compare it against the browser's total load time: when the two are far apart, the difference is transfer and rendering rather than your server-side code.

A static asset served from cache typically reports single digits. A page your server-side code renders reports whatever that render costs, plus a startup if the request arrived cold — see the section above.

Time limits for server-rendered routes

A server-rendered page or route handler has 60 seconds to start its response. One that has not started by then gets a 504 at 60 seconds, on every method:

curl -sS -o /dev/null -D - "https://your-site.frontends.volcano.run/api/report"
# HTTP/2 504
# x-volcano-origin-ms: 60012

The request is not retried, so your route runs once per request. The 504 does not stop it, though: it keeps running until it returns or reaches the runtime limit below. Put a timeout on the outbound calls a route makes so it can fail inside the window with an error of your own.

LimitHOBBYSUPERAGENT
Time to start a response60 s60 s
Time between chunks of a streamed response60 s60 s
Total time the runtime gives one request30 s180 s

On HOBBY the runtime limit comes first, so a route that has not answered fails at 30 seconds. Next.js's maxDuration export does not change any of these.

Stream work that takes longer than 60 seconds

A streamed response is not held to the 60-second start: it only has to send something at least every 60 seconds, for as long as the runtime limit allows. Send the headers and a first chunk right away, then the rest as it is ready:

// app/api/report/route.js
export const dynamic = 'force-dynamic';

export async function GET() {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for (let step = 1; step <= 9; step++) {
        controller.enqueue(encoder.encode(`data: step ${step}\n\n`));
        await new Promise((resolve) => setTimeout(resolve, 10_000));
      }
      controller.enqueue(encoder.encode('data: done\n\n'));
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-store' },
  });
}

That route answers for 90 seconds on SUPERAGENT. On HOBBY it is cut off at 30. For work that has to outlast the runtime limit, start a durable function and poll its result.

Call outside APIs from server code

Server-side code in a frontend can call any HTTPS API directly. Volcano does not restrict outbound traffic, so a route handler can call a model provider with a key from a selected variable:

// app/api/chat/route.js
export async function POST(request) {
  const { messages } = await request.json();
  const upstream = await fetch('https://api.example-llm.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      authorization: `Bearer ${process.env.LLM_API_KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ model: 'example-model', messages, stream: true }),
    signal: AbortSignal.timeout(25_000),
  });
  return new Response(upstream.body, {
    status: upstream.status,
    headers: { 'content-type': upstream.headers.get('content-type') ?? 'text/plain' },
  });
}

Select LLM_API_KEY for the frontend and keep it out of the NEXT_PUBLIC_ prefix; see Variables. The call is held to the time limits above, so stream the reply and time out the upstream request inside your plan's runtime limit.

Volcano does not store request or response bodies, so a prompt reaches your runtime logs only if your code logs it. Runtime logs are kept for your plan's log retention.

Move the call to a function when several frontends or other clients share it, or when it needs a runtime other than Node.js. Move it to a durable function when one request can outlast the runtime limit.

Platform error pages

When Volcano cannot route or serve a frontend request, browsers receive a self-contained HTML error page. The status code identifies the failure:

StatusMeaning
403Project traffic is disabled.
404No deployed frontend serves the address, or the requested /_next/static file is in no build the frontend still serves.
429The project reached a rate or monthly request limit, or one visitor sent too many server-rendered or function-route requests.
502The deployed frontend is temporarily unreachable.
503A platform dependency is temporarily unavailable.

A 429 sent because one visitor exceeded the per-visitor limit has the code viewer_rate_limited and a Retry-After header with the seconds until that visitor can send again. Visitors at other addresses are not affected.

The page may include a reference ID for support. 502 and 503 pages link to the platform status page. Clients that prefer application/json receive a JSON error response instead; Accept quality values are honored.

Read the visitor's address

Your server-side code finds the visitor's IP address in the usual headers:

// app/api/whoami/route.js
export function GET(request) {
  return Response.json({ ip: request.headers.get('x-real-ip') });
}
HeaderValue
X-Real-IPThe visitor's address
X-Forwarded-ForThe visitor's address, followed by Volcano's own hops

Volcano replaces any X-Real-IP or X-Forwarded-For the visitor sends, so neither can be spoofed. Use the first X-Forwarded-For entry, not the last.

Headers starting with CloudFront- are removed before your code runs. Their address and location values describe Volcano's network rather than the visitor. To detect the visitor's device, read User-Agent. A frontend deployed before they were removed receives them until its next deployment; do not rely on them.

Variables

A frontend gets the project variables it selects. New frontends select none; choose them with variable_scope and variables in the declarative config or on deploy.

Selected variables are available to the build. Every selected variable except NEXT_PUBLIC_* is also set in the deployed frontend's server runtime, which is capped at 4096 bytes including about 900 bytes Volcano uses itself (see Environment size):

VariableBuildServer runtime
NEXT_PUBLIC_*YesNo. Next.js inlines the build-time value wherever your code reads process.env.NEXT_PUBLIC_… literally, in browser and server code alike
A name Volcano sets in the runtime, listed belowYes, unless the build reserves itNo. Volcano's value replaces yours
Any other selected nameYes, unless the build reserves itYes, including other names the build reserves, such as VOLCANO_*
A name you did not selectNoNo

Changing a NEXT_PUBLIC_* value takes a redeploy. Any other value reaches the running frontend without one. Read secrets such as API keys in server code only, under a name without the NEXT_PUBLIC_ prefix, so they never enter the browser bundle.

Volcano sets these runtime variables itself:

VariableValue
VOLCANO_FRONTEND_IDThe frontend's ID
VOLCANO_FRONTEND_NAMEThe frontend's name
VOLCANO_FRONTEND_FRAMEWORKThe frontend's framework, such as nextjs
NODE_ENVproduction

It also sets the frontend's cache wiring. FRONTEND_PROXY_TOKEN and FRONTEND_PROXY_NEXT_TOKEN hold Volcano's own credential and are removed before your code runs, so a selected variable with either name never reaches it. Nothing else is set for you. Even the project's DATABASE_URL is a variable you create and select.

Local mode differs: the frontend reads its variables only when it starts and gets no VOLCANO_FRONTEND_* names.

A deploy that selects a name the project does not define is rejected before anything builds. The deploy endpoint answers 400 with an error that ends in the missing names:

frontend "web": declared project variables do not exist: STRIPE_SECRET_KEY

A config apply answers 422 with the same message under the frontend:

{
  "error": "project config validation failed",
  "errors": [
    {
      "section": "frontends",
      "name": "web",
      "message": "frontend \"web\": declared project variables do not exist: STRIPE_SECRET_KEY"
    }
  ]
}

See the frontend API reference.

Function routes

A frontend can send a path such as /api/session to a public HTTP-mode function, so your pages call it on the same origin and can keep a session in HttpOnly cookies. See Frontend Function routes.

Frontend routes

A frontend can serve another frontend of the project under a path, such as /docs, so one hostname runs several apps. See Frontend routes.

Custom domains

Custom domains are available on every plan and support Volcano-managed TLS or bring-your-own-certificate (BYOC) TLS. With managed TLS, Volcano issues and renews the certificate after you publish the returned validation records.

Your account must own a domain before a frontend serves it. Verify a domain once with a _volcano TXT record, and every hostname below it attaches to any of your frontends, in either TLS mode, without another record. See Verify domain ownership.

A frontend can have several custom domains. Each one either serves the frontend or redirects to another of its domains, so one deployment can answer on www.example.com and app.example.com and send example.com to www. HOBBY allows one custom domain per frontend and SUPERAGENT allows 10. Adding one past the limit returns 403. Managed TLS certificates are limited per account: HOBBY includes one and SUPERAGENT includes 100. A removed managed domain holds its certificate until Volcano finishes removing it, which takes a few minutes; adding a managed domain past the limit before then also returns 403. See Plans and limits.

Without a certificate, Volcano manages TLS for the domain:

volcano cloud frontends domains add my-site app.example.com
volcano cloud frontends domains get my-site app.example.com

A 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: managed

If your account already owns the domain, the verification records name the certificate validation CNAME right away. Otherwise they name a _volcano TXT record for the hostname's registrable domain, such as _volcano.example.com for app.example.com. Publish it; Volcano holds the hostname for your account while it waits. Once the account owns the domain, Volcano returns the CNAME instead. Add the CNAME and keep it in DNS for issuance and renewal. Keep the TXT record too: the domain moves to another account only once DNS stops serving it, or serves only a value from before a key rotation. See Move a domain to another account.

To use BYOC instead, attach the domain with your certificate, then point DNS at the frontend. A certificate does not prove ownership, so BYOC also needs a verified domain; until then the attach fails with 409 and the required_record to publish:

# 1. Attach the domain with your PEM certificate + unencrypted private key
#    (--chain is optional; include it if your CA requires the issuing chain).
volcano cloud frontends domains add my-site app.example.com \
  --cert  ./fullchain-leaf.pem \
  --key   ./privkey.pem \
  --chain ./chain.pem

# 2. Check its status.
volcano cloud frontends domains get my-site app.example.com

You can also attach custom domains declaratively — see custom_domain and custom_domains in the configuration reference.

Removing a domain takes about a minute for BYOC and a few minutes for managed TLS, while Volcano detaches it and releases the hostname. Until then, attaching the hostname again, to this or any other frontend, returns 409 Conflict with code: custom_domain_releasing and a Retry-After header, and so does attaching another domain to this frontend with volcano cloud frontends domain create. Send the same request again after that many seconds. If Volcano cannot finish the removal within a few minutes, the 409 has no code; contact support if it persists.

Redirect one domain to another

Give a domain a redirect and Volcano answers every request for it with a redirect to another domain of the same frontend, keeping the path and query string. The redirect never reaches your app. The usual case sends the apex to www. A redirect can only point at an active domain, so attach both first:

version: 1
frontends:
  - name: my-site
    custom_domains:
      - domain: www.example.com
        tls:
          mode: managed
      - domain: example.com
        tls:
          mode: managed

Once www.example.com is active, add the 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.com # an active domain of this frontend that serves it
          status: 308 # 301, 302, 307, or 308; default 308
volcano cloud config deploy
curl -sI 'https://example.com/pricing?plan=pro'
HTTP/2 308
location: https://www.example.com/pricing?plan=pro

Or with the CLI:

volcano cloud frontends domains add my-site www.example.com
volcano cloud frontends domains list my-site   # wait until www.example.com is active
volcano cloud frontends domains add my-site example.com --redirect-to www.example.com

301 and 308 are permanent, and browsers cache them; 302 and 307 are temporary. 307 and 308 keep the request method and body, so a form posted to the apex still arrives as a POST. Use 302 while you test, then switch to 308.

A redirect domain needs everything a serving domain needs: ownership, a certificate, and DNS pointing at the frontend, because the browser connects to it over HTTPS before it can read the redirect. A redirect must point at an active domain that serves the frontend, not at another redirect or a domain still waiting for DNS or its certificate; Volcano refuses with 400. If the target stops serving later, the redirect domain serves the frontend until the target is active again. If Volcano removes the target instead, it clears the redirects that pointed at it, and they do not come back when the hostname returns; set them again. A domain that others redirect to cannot be removed or made a redirect itself until they point elsewhere; Volcano refuses with 409 and names them. To swap which domain serves, change both in one volcano cloud config deploy, which orders the steps so a redirect never points at a domain that isn't serving.

Point DNS at your frontend

Check 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 frontend's default Volcano hostname (the <frontend-id>.frontends.<region-domain> host shown by volcano cloud frontends get). That host is stable across redeploys.

For a provider-confirmed non-apex:

app.example.com.  CNAME  <frontend-id>.frontends.volcano.run.

Every custom domain of the frontend points at the same host, including the ones that redirect. For an apex redirecting to www:

www.example.com.  CNAME  <frontend-id>.frontends.volcano.run.
example.com.      ALIAS  <frontend-id>.frontends.volcano.run.

The domain becomes active once Volcano finishes attaching the certificate. That status does not confirm your DNS is live. For a confirmed non-apex, inspect the CNAME with dig CNAME app.example.com; for an apex, check the ALIAS, ANAME, or flattening record with your DNS provider.

To rotate the certificate later, update it through the declarative custom_domain config and re-apply; the rotation is zero-downtime. Re-running domains add for a BYOC domain that's already attached is a no-op and won't replace the certificate.

Wait out the switchover

active means Volcano has the certificate and is rolling the hostname out. Allow a short window before every request reaches your site:

  • Volcano serves the hostname within seconds of active, and within a minute at most. Until then an HTTPS request to it fails the TLS handshake.
  • Resolvers keep sending visitors to your previous DNS target until the old record's TTL runs out.

Don't roll back during that window. Every Volcano response carries an x-volcano-version header, so an HTTP error without one came from your previous host. -S keeps a handshake failure visible:

curl -sSI https://app.example.com/ | grep -i -E '^(HTTP|x-volcano)'

Switch between BYOC and managed TLS

An active domain switches between BYOC and managed TLS in place, without downtime. The current certificate keeps serving until its replacement does.

To move a BYOC domain to managed TLS, attach it again with --tls managed:

volcano cloud frontends domain create my-site --domain app.example.com --tls managed
volcano cloud frontends domain get my-site

Your uploaded certificate keeps serving while Volcano issues the managed one. Publish the _acme-challenge CNAME from verification_records. The domain stays active, and serving_certificate changes from uploaded to managed once the managed certificate serves. If issuance fails, failure_reason names the cause while the uploaded certificate keeps serving. If the uploaded certificate expires first, Volcano stops serving it. The domain returns to pending_verification, or to failed with failure_reason if issuance had failed, until the managed certificate is issued. The switch needs a free managed TLS certificate in your plan.

To move a managed domain to BYOC, attach your certificate to the same hostname:

volcano cloud frontends domain create my-site \
  --domain app.example.com \
  --cert  ./fullchain-leaf.pem \
  --key   ./privkey.pem

Your certificate replaces the managed one at once, and Volcano stops renewing the managed certificate. You can then delete the _acme-challenge CNAME.

If volcano-config.yaml declares the domain, keep its tls.mode in step with the switch. Otherwise its next deploy switches the domain back or, for mode: byoc without certificate fields, fails. To switch in the manifest itself, change tls.mode and run volcano cloud config deploy. Switching to managed there means removing every PEM field; switching to BYOC needs certificate_pem and private_key_pem. Once a domain is BYOC, tls.mode: byoc without them keeps its certificate.

A domain that is not yet active has no certificate to keep serving. Wait until it is active, or remove it and attach it again with the other mode.

Next

On this page