Skip to main content
Astro is a server-first framework. It renders HTML on the server, and it ships only the JavaScript a page really needs. The @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

See installation for npm, Bun, and Windows.
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:
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 bunny sites deploy at the repository root, and it finds the projects below it:
Or run it in the project’s own directory, and it deploys that one. 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 set output. 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

A deploy with no changes does nothing, and says so. Every deploy publishes to production; a preview URL per branch is not built yet.

Rolling back

Each deploy keeps both halves in the storage zone: the files, and the server bundle that was built against them. Neither is ever overwritten, so a rollback restores a matched pair in one step. It takes a few seconds to reach every edge node. The CLI keeps the newest 5 deploys. 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
Every developer and every workflow then deploys the same way, with no flags. Build-time variables come from the command line:
They apply to the build, so they need --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

Use npm 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:
It needs Deno 2, because Deno is the Edge Scripting runtime. The 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 script sees every request the cache does not answer. It renders the routes Astro owns, and reads everything the build produced out of Bunny Storage. The adapter inlines the list of built files into the script, so a request for a path the build never produced costs no lookup at all. Each deploy is immutable, and it has two halves in the same storage zone: 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 gets Astro.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’s routeRules 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

That calls the purge API, which is an account operation. The CLI sets the pull zone id for you. The key is yours to add, because it is account-wide:

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.
Pass sessions: false to configure your own driver.

Images

Set imageService: "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
Image transformation on a script-backed site is not ready yet. Turn Optimizer on today and an image request that misses the cache can answer 523 Origin Connection Failed. We are fixing it. Until then, leave imageService at its default: the parameters are ignored, and the original image is served.
Optimizer only works on files your own pull zone serves, so an image on another host passes through untouched.

Large files

A stored object can be fetched in pieces. The script answers a Range 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:
The script also passes 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 most node: 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, so bunny() on its own is usually right.
astro.config.mjs
No credential is ever an option. The CLI sets each one on the script, so nothing reaches the bundle or your configuration file.

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.
That creates a new site: its own storage zone, script, and pull zone, and its own URL. Adopting the zone and script you already have is not built yet, so until it is, either move to the new URL (add your domain to it with bunny sites domains add) or keep deploying the old pair by hand:
A deploy you run yourself puts the files at the zone root, which is where the script looks when nothing sets 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:
Files alone cannot answer a 404 with a page, send a redirect, or add a header. The 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-staging from 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 deploy creates 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.
The design for all three is in plans/one-command-deploys.md.

Troubleshooting

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.
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.
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.
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.
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.
The pull zone is stripping the header. bunny sites deploy turns Disable cookies off for you. Run bunny sites deploy again, and check the settings it reports.
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.
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.
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.
The site’s router is older than the CLI that reads your 404.html. Update it, and deploy again:
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.
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.
Optimizer cannot transform images for a script-backed site yet. Leave imageService at its default. See Images.
Last modified on August 21, 2026