bunny sites hosts a site on bunny.net. Each site is three resources, provisioned and wired together for you:
Zones are named
sites-<name>-<suffix>. The prefix groups them in the dashboard. The suffix is there because zone names are global across bunny.net. The commands take the clean site name.
The Edge Script is what the two kinds of site differ by:
- A static site’s script is the router this CLI writes. It maps each request to the deploy that answers it, and it serves the deploy’s own
404.html,_redirectsand_headers. - A site that renders per request runs the build’s own server in the script. Its client files still come from Bunny Storage, and each deploy keeps its own server bundle, so publishing an earlier deploy restores its pages and its assets together.
bunny sites deploy reads .bunny/build.json, the manifest a framework adapter writes, and takes the path it names. The CLI knows no framework: it reads the manifest, so a new adapter needs no new CLI.
Deploy this project
1
Log in
2
Run the command in your project
3
Add your domain
The site’s first deploy offers one. To add it later:
A project that renders per request
The build writes.bunny/build.json, and the deploy acts on it. It uploads the client files and publishes the script. It also applies the pull zone settings the adapter asks for, and sets the variables the script reads.
Then it asks the live site for a page. A green line above a URL that answers 400 is the worst thing this command can do, so a script that does not start is reported instead:
bunny sites create provisions a static site. For a project that renders per
request, let bunny sites deploy create the site: it needs the build manifest
to know what the site runs, and create runs no build.When the adapter is offered
Only a project that asks for a server hears about an adapter. Any one of these counts as asking:
A project with none of these prerenders every page. It deploys as a directory of files, it is never offered an adapter, and its config is never edited.
The last signal is the one that matters most. Since Astro 5 a project prerenders every page unless a page opts out, and
astro build stops with its own error when a page opts out and no adapter is installed. Reading the routes puts the offer before that failure.
A build that renders nothing
A build with no route to render per request writes no script at all. The deploy is the files, and the site is served by the static router below: it answers a miss with the build’s own404.html, and it reads _redirects and _headers from the deploy.
The build says which of the two it produced, and why.
Deploy a directory
1
Deploy
2
Publish
Immutable deploys
Every deploy uploads to its own directory in the storage zone, and gets a pull zone of its own. That is a permanent HTTPS URL, and it needs no DNS or certificate setup:X-Robots-Tag: noindex.
The deploy ID is the git short SHA when the working tree is clean, and a content hash otherwise. So re-deploying identical content is a no-op. Use --force to override that.
What the deploy configures
A static site’s router reads three file names out of the deploy it is serving. Cloudflare Pages and Netlify read the same three. So a build that already writes them works here unchanged, and the router knows no framework.
A site that renders per request gets none of this. Its script is the build’s own server, and the build decides what it answers with.
_redirects
One rule per line: a path, a target, and an optional status. Lines starting with # are comments.
- The default status is
301.302,303,307and308are read too. A rewrite (200) is not supported. - A trailing
*in the path is captured, and:splatin the target is replaced with it. - A rule applies only where the deploy holds no file at that path, so a real file always wins.
!after the status forces the rule ahead of the file. - A path matches with or without its trailing slash, so
/aboutand/about/are one rule.
_headers
A line starting with / opens a block, and the indented Name: value lines below it belong to that block. Where two blocks match one path, the later one wins that header name.
Caching
The router setsCache-Control on every response, because Bunny Storage sends none for HTML:
Publishing purges the whole zone, so a promoted deploy answers at once. A purge does not reach a browser, which is why a page keeps only the one minute. A file whose name carries a content hash never changes. Name that directory in
_headers, and the browser keeps it for a year.
A site created by an earlier CLI has an earlier router.
bunny sites deploy
republishes it when it lags, and bunny sites upgrade-router does it on
demand.Custom domains
A custom domain is the site’s production URL. Previews never depend on one.Every command
Which site a command acts on
Most commands take the site as an optional positional.deploy, ci init, and deployments publish use --site. Either accepts the site name or its storage zone ID.
With none given, the site resolves in this order:
- The directory’s linked site, in
.bunny/site.json sites.nameinbunny.jsonc- An interactive picker, which offers to link the directory
--output json, no TTY, or --force on a destructive command. A --name is an instruction, so an unattended run with one creates the site and needs nothing else.
A build and a site have to be the same kind. A script’s type is fixed when it is created, so a static site cannot run a build’s server, and a site that renders per request cannot serve a directory of files. The deploy names the mismatch, and it stops before it uploads anything.
Preconfigure the sites block in bunny.jsonc and a deploy needs no arguments:
dir here is an instruction: deploy these files. The command then offers no adapter, and it deploys the directory whatever the project holds.
Site state lives at _bunny/site.json inside the storage zone, which the router never serves. .bunny/site.json is only a local pointer. So a fresh clone can bunny sites link, and go on where the last machine stopped.
In a monorepo, run
bunny sites deploy at the repository root, and it finds the
framework projects below it. Or run it in a project’s own directory to deploy
that one. The package manager comes from the workspace, so a pnpm monorepo
gets pnpm.