@bunny.net/astro-adapter package runs that server inside an Edge Script. Astro renders pages and API routes per request, on a node near your visitor. The build’s static files live in Bunny Storage, and the script serves them from there.
One command does all of it:
bunny lab holds commands we are still shaping, so the flags and the output
can change between CLI releases. There are two, and no more: bunny lab deploy astro and bunny lab undeploy astro.See it running
A live Astro site on Edge Scripting. Every page demonstrates one adapter
capability, and the source is on
GitHub.
Deploy your first site
1
Install the CLI and log in
2
Run bunny lab deploy astro
From your Astro project:The command puts the adapter in, builds the site, creates what the site needs, and publishes it:Open the URL. Reload it, and anything the page renders per request changes each time.The first prompt installs the adapter from npm. To add it yourself, run
npx astro add @bunny.net/astro-adapter before you deploy.The app’s name comes from your package.json. Pass --name to choose one.3
Take it down again
One command deletes the pull zone, the Edge Script, and the storage zone:It lists the three resources, and asks you to type the app’s name.
--keep-storage keeps the files.Nothing above asks for a password. The CLI creates the storage zone, so it
already holds the credentials, and it sets them on the script as
secrets.
What the deploy created
All three are named after your app:
astro-my-app-k3f9wq. The suffix is there because a storage zone name and a pull zone name are unique across all of bunny.net.
The CLI also applies the pull zone settings the adapter asks for, and it says which ones it changed: cookies pass through, so Astro.cookies.set() works, Smart Cache goes off so the adapter’s own cache headers count, and the zone’s cache override goes off so those headers reach a visitor unchanged.
Add your own domain
The two commands do not attach one. Add it to the app’s pull zone in the dashboard, under CDN > your pull zone > Hostnames, and the free certificate is one click from there.What the deploy needs from you
Three things, and only the first is usually a surprise:
Code your project may need to change covers all three, with what each one looks like when it goes wrong.
Moving from another host
A project that already has an adapter keeps everything but that one line. The command names the adapter it found, swaps it in the config, and uninstalls the package:@astrojs/node@9 requires astro@^5, so leaving it in package.json after an upgrade to Astro 7 makes every later npm install in that project fail, with an error about peer ranges that says nothing about adapters.
Adapter options do not carry over, because no two hosts share them. Anything the old adapter was configured with is worth reading again in the options below.
In a monorepo
Run the command at the repository root, and it finds the projects below it:bunny lab deploy astro ./docs. The package manager comes from the workspace, not from the directory, so a pnpm monorepo gets pnpm.
What the adapter does not change
The adapter adds itself to your config, and nothing else. It does not setoutput.
Since Astro 5, a project that says nothing about output prerenders its pages, and a page asks for the edge with export const prerender = false. That default is the right one, and setting output: "server" over it is expensive: on astro.build it turned 4499 prerendered pages into pages that render per request, and took the script from 7.83 MB to 22.30 MB, past the limit.
So the build asks Astro what it built, and Astro answers from the routes:
The build says which one it chose. A site can also ask for the script outright. That is what a prerendered page holding a
server:defer component needs, because Astro reports such a project as a fully prerendered one:
astro.config.mjs
Code your project may need to change
Most Astro projects deploy as they stand. Three things do not, and no deploy command can fix them for you. Bothwithastro/astro/examples/ssr and
render-examples/astro-ssr met at least one.
Astro 7
The adapter’s peer range isastro@^7.0.0. An older project stops before
anything is installed:
render-examples/astro-ssr ships Astro 5, so it needs this step. Without the
check, npm install answers with an ERESOLVE about peer ranges that names no
version to move to.
State in a module does not survive a request
Each request may reach a different edge node, and a different isolate. So a value held at module scope is gone by the next request:withastro/astro/examples/ssr keeps its shopping cart in exactly that. Deployed
as it stands, the cart accepts every item and shows none:
Astro.session instead. It writes to the app’s own storage zone, so
every node reads the same value, and the deploy sets it up:
Do not fetch your own site to reach your own route
A page that calls its own API route over HTTP works, and costs more than it looks:withastro/astro/examples/ssr does this on every page. Import the
data instead, and the page renders in one pass:
A form POST answers 403
That is Astro, not the platform.security.checkOrigin is on by default for
output: "server", and it rejects a form POST whose Origin header does not
match the site. A browser always sends the header; curl has to be told to:
security.checkOrigin
is where to turn it off, if a machine client really has to POST a form.
The everyday loop
Configure the app
.bunny/astro.json is all a repeat deploy needs, and the command adds .bunny/ to your .gitignore. A deploy from a fresh clone, or from CI, names the app instead:
astro:env reads them:
.bunny/astro.json keeps.
Continuous integration
A deploy needs an API key and nothing else, so a workflow is three lines:.github/workflows/deploy.yml
--name says which app to deploy to, because .bunny/ is git-ignored and a runner has no state file. --yes lets the command install the adapter and edit the Astro config without asking: without it, an unattended run prints the two changes and stops, rather than rewriting your source in silence.
Commit the adapter and the config change once, from your own machine, and the workflow then needs neither flag’s permission — only --name.
Add --output json when a later step reads the result. See GitHub Actions for the details.
Run it locally
Usenpm run dev for everyday work on the site. It is Astro’s dev server, and the adapter stays out of the way.
npm run preview runs the file you are about to deploy. A local storage zone stands in for Bunny Storage, so assets, prerendered pages, and sessions all work with no account and no network:
cdn- request headers only exist on the bunny.net network, so anything your code reads from them is absent locally.
A build with no script has no file to run, so npm run preview serves dist/client from Astro’s own static server. That needs no Deno.
How it works
The CLI writes the deploy’s own folder name into the top of the bundle it publishes. So a published version can only read the files it was built with, which is what makes a rollback restore a page and its assets together. An Astro server bundle names the hashed CSS file it renders, so the two really are one unit.
What runs where
The edge context
Every page getsAstro.locals.runtime, which carries what the bunny.net network knows about the request:
src/middleware.ts
Add the types to your project:
src/env.d.ts
Caching
Astro’srouteRules become the headers the CDN reads:
astro.config.mjs
s-maxage is for the CDN and max-age is for the browser. Splitting them is what makes a purge take effect straight away.
A page that sets no Cache-Control gets private, no-store. That matters: a pull zone applies its own expiration to a response that carries no directive, and a page rendered for one visitor must never reach another. A route with a routeRules entry keeps what the rule gave it.
Smart Cache only caches known static file extensions, and
HTML is not one of them, so a
routeRules entry does nothing while it is on.
bunny lab deploy astro turns it off for you when your project sets
routeRules, and reports the change.Purging
Sessions
Astro.session works with no setup. Each session is one object in the site’s storage zone, so every edge node reads the same value, and nothing serves those objects to the web.
Bunny Storage does not expire an object, so
session.ttl controls the cookie
and not the stored object. Delete old objects yourself if the zone grows.sessions: false to configure your own driver.
Images
SetimageService: "bunny" and Astro’s <Image> component uses Bunny Optimizer, which resizes and re-encodes at the edge. There is no build step, and no sharp:
astro.config.mjs
Large files
A stored object can be fetched in pieces. The script answers aRange request with 206 and a Content-Range, and it says Accept-Ranges: bytes on everything it serves out of storage.
That header is the part that matters. A pull zone will not answer a range from its cache, and will not slice an object, unless the origin says it accepts ranges. Without it a video is only seekable once it is fully cached.
For a large file that nobody has requested yet, turn on Optimize for large object delivery in the pull zone’s caching settings, and the zone fetches the object in chunks so the first request is seekable too:
If-None-Match and If-Modified-Since through, so a browser and the pull zone both revalidate with a 304 instead of downloading the object again. See range requests for what the CDN does with them.
Supported features
Node built-ins
Edge Scripting provides mostnode: modules, so a dependency that imports one is usually fine.
node:fs works over a virtual file system. It starts empty on every cold start, one isolate cannot see what another wrote, and what it holds counts against the script’s memory. So it is a scratch pad for one request, and never a store. Anything that has to outlive a request belongs in Bunny Storage, which is where the adapter keeps files and sessions.
A package with a native binary never works. sharp is the usual one, and the adapter already replaces it.
Adapter options
Every option has a sensible default, sobunny() on its own is usually right.
astro.config.mjs
Moving from bunny-astro
Earlier versions of the adapter shipped a bunny-astro command.
bunny lab deploy astro replaces it, and adds provisioning and teardown.
BUNNY_ASSET_PREFIX.
A site with no script
A site whose routes are all prerendered needs no script, and no adapter.bunny sites deploy uploads the build and publishes it, every deploy stays immutable under its own ID, and nothing is invoked per request. The build says so:
bunny sites router does all three, for every framework. It learns what to do from three file names, which Cloudflare Pages and Netlify read too:
So keeping the adapter on a prerendered site is worth it for those two files. It costs nothing at run time.
Astro also writes its own
<meta http-equiv="refresh"> page for each redirect. A deploy that never reaches the router therefore still sends the visitor on. The rule in _redirects carries !, and that is what makes the router’s real 301 win over the page.
npm run preview on such a build serves dist/client from Astro’s own static server, and needs no Deno.
A site with no adapter at all works too, and the static site hosting guide covers it. What it loses is those two files: a redirect becomes a <meta> refresh, and a header cannot be set.
What is not here yet
Three things this guide’s commands do not cover:- Rollback. One Edge Script publishes one version at a time, and the two commands keep no older version to return to. Deploy the commit you want instead:
git checkout <commit> && bunny lab deploy astro. - A preview URL per branch. Every deploy publishes to production. A preview has to be its own Edge Script, because a page and the files it names are one unit. Until then, deploy a second app for staging:
bunny lab deploy astro --name my-app-stagingfrom a checkout of the branch. - A custom domain from the command line. Add it to the app’s pull zone in the dashboard, under CDN > your pull zone > Hostnames.
- Adopting an app you built by hand. The command creates its own storage zone, script, and pull zone. There is nothing yet that takes over a pair you made yourself.
- Logs from the command line. Read them in the dashboard, under Scripting > your script > Logs.
Troubleshooting
The site answers 400 with an empty body
The site answers 400 with an empty body
The script did not start, so nothing answered the request. A large script is
the usual reason, and 10 MB is not the size to aim at: a script has
500ms to start, and every byte of it is parsed and
evaluated first.Measured in August 2026, on a standalone script in DE: the same code served
every request at 7.44 MB, and answered 400 to all of them at 7.83 MB. Around
7.5 MB the first request failed and later ones worked. How much code fits
depends on what it does as it loads, so treat 7.5 MB as a warning and not a
line.The build warns above 7.5 MB, and
bunny lab deploy astro asks the site for a
page before it reports success, so neither is silent about it. What to do is
the same as for a script that is too large, below. Deploy the last commit that
worked to put the site back while you work on it.The build says the script is too large
The build says the script is too large
Edge Scripting takes one file of up to 10 MB, so a build that produced more
than that fails, and names the packages that filled it:Two things usually help. Prerender the routes that do not need a server, so
that the pages which pulled a package in stop being part of the script. And
keep a heavy dependency out of a page, and out of anything a page imports: a
package that only runs at build time does not belong in the script.A small Astro site lands near 660 kB. Aim below 7.5 MB, not below 10 MB: see
the 400 above.
The script is too slow to start
The script is too slow to start
A script has 500 ms to start. Move work out of module scope and into the
request handler, and drop dependencies you do not need. See the
limits.
Astro.rewrite('/404') throws a 500
Astro.rewrite('/404') throws a 500
A prerendered route has no server component to rewrite to. Return
new Response(null, { status: 404 }) from the page instead, and your
prerendered 404 page is served out of Bunny Storage.An endpoint in src/pages/api/ keeps its own 404, so a client asking for
JSON is never handed a web page.A routeRules entry changes nothing
A routeRules entry changes nothing
Smart Cache is on, and it does not cache HTML.
bunny lab deploy astro turns
it off and says so, so run the deploy again and read what it reports.A page shows another visitor's content
A page shows another visitor's content
Something removed the
Cache-Control the adapter sets. Check that no edge
rule overrides it, and that the page is not setting a public directive
itself.A POST request returns 403
A POST request returns 403
Astro checks the request origin for server output, and rejects a form POST
whose
Origin header does not match the site. This is Astro’s CSRF
protection, not an Edge Scripting error. Send the request from the site
itself, or change security.checkOrigin in astro.config.mjs.The bundle fails on a Node-only module
The bundle fails on a Node-only module
Most
node: built-ins work here, including
node:fs, so check which module it is. The adapter
rewrites a bare fs to node:fs for you, because the runtime only answers
to the prefixed name.A package with a native binary never works. Replace that dependency, or
keep it out of the server build.A missing path shows bunny.net's error page, not mine
A missing path shows bunny.net's error page, not mine
The request never reached the script, so Astro’s own
404.astro never
rendered. Read the script’s logs in the dashboard, under Scripting > your
script > Logs: a script that fails as it starts answers this way.A deploy checks this for you: it asks the live site for a path the build
cannot hold, and warns when the answer came from bunny.net rather than from
Astro.A page 404s after a deploy, and the old one worked
A page 404s after a deploy, and the old one worked
Deploy the last commit that worked, and compare the two builds:A route that renders per request answers from the script, so check the
script’s logs first. A prerendered page answers from the storage zone, so
check that the build still writes it.
Images return 523
Images return 523
Optimizer cannot transform images for a script-backed site yet. Leave
imageService at its default. See Images.