@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:
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 sites deploy
From your Astro project:The CLI reads your project. A project that renders a page on demand is offered the adapter, and the command then 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.
3
Add your domain
The first deploy offers one. To add it later:The CLI adds the hostname, prints the
CNAME record to create, waits for DNS, and issues a free certificate.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
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, and Smart Cache goes off so the adapter’s own cache headers count.
When the adapter is offered
Only a project that asks for a server is offered one. Any one of these counts as asking:
A project with none of these prerenders every page.
bunny sites deploy uploads the build, the site is served as files, and the adapter is never mentioned. That is the right answer for such a site: a site with no server should not carry a megabyte of one.
The first signal is the one that matters most. astro build stops with its own error when a route opts out of prerendering and no adapter is installed, so the offer arrives before that failure.
Moving from another host
A project that already has an adapter keeps everything but that one line.bunny sites deploy names the adapter it found, and swaps it:
In a monorepo
Runbunny sites deploy at the repository root, and it finds the projects below it:
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
The everyday loop
Rolling back
bunny sites deployments prune clears the rest.
Configure the site
.bunny/site.json is all a repeat deploy needs. Add a bunny.jsonc when you want the site’s name and build command in version control:
bunny.jsonc
--build. A deploy without it says so rather than ignoring them.
Runtime variables belong to the script, and astro:env reads them:
bunny sites show prints the script’s ID, and the commands above use the script this directory is linked to.
Continuous integration
A deploy needs an API key and nothing else, so a workflow is three lines:.github/workflows/deploy.yml
--build is what runs the build here. A workflow has nobody to ask, so an unattended deploy never offers one, and it deploys what the build wrote.
Commit .bunny/site.json so the workflow deploys the same site you do, or pass --name <site>. 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 sites deploy 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 sites deploy replaces it, and adds provisioning, immutable deploys, and rollback.
bunny sites domains add) or keep deploying the old pair by hand:
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, every deploy gets its own immutable URL, 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:- A preview URL per branch. A build that renders per request publishes to production, and the command says so. Such a preview has to be its own Edge Script, because one script publishes one version at a time, and a page and its assets are one unit. Until then, deploy a second site for staging:
bunny sites deploy --name my-site-stagingfrom a checkout of the branch. A build with every route prerendered is a static deploy, and every one of those already gets its own preview URL. - Adopting a site you built by hand.
bunny sites deploycreates its own storage zone, script, and pull zone. There is no command yet that takes over a pair you made yourself. - Logs from the command line. Read them in the dashboard, under Scripting > your script > Logs.
plans/one-command-deploys.md.
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 sites deploy 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. bunny sites deployments publish --previous puts the
previous deploy 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 sites deploy 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 site’s router is older than the CLI that reads your A deploy checks this for you now: it asks the live site for a path the build
cannot hold, and warns when the answer was not your own page.
404.html. Update
it, and deploy again:A page 404s after a deploy, and the old one worked
A page 404s after a deploy, and the old one worked
Roll back while you look:
bunny sites deployments publish --previous.
Each deploy keeps its own files
and its own bundle, so the previous version comes back whole.Images return 523
Images return 523
Optimizer cannot transform images for a script-backed site yet. Leave
imageService at its default. See Images.