Skip to main content
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, _redirects and _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.
One command deploys both. 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

With no site linked yet, the command offers to create one. A project that renders pages on demand also gets an offer to add the bunny.net adapter for its framework:
3

Add your domain

The site’s first deploy offers one. To add it later:
Nothing above asks for a password. The CLI creates the storage zone, so it already holds the credentials. It sets them on the script as secrets. Every deploy goes to its own directory in the storage zone. So publishing an earlier one restores its pages, and the files those pages name, together.

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:
One script serves one release, so such a deploy publishes to production, and the command says so. Preview environments for a site that renders per request are not built yet.
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 own 404.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

The deploy lands on its own preview URL, and the first deploy of a site offers to publish it.
2

Publish

Publishing points the router at that deploy and purges the cache. Nothing moves, so going live and rolling back are both instant.

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:
A preview serves from the site’s root, not under a path prefix. So a client-side router and a root-absolute asset behave exactly as they do in production. Preview responses carry 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.
A rollback of a site that renders per request reads that deploy’s stored server bundle back out of storage. So the pages and the files they name come back together.

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.
Without a 404.html in the deploy, a missing path answers with bunny.net’s own error page instead of your site’s. So bunny sites deploy asks the published site for a path it cannot hold. It warns you when the answer is not your page.
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, 307 and 308 are read too. A rewrite (200) is not supported.
  • A trailing * in the path is captured, and :splat in 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 /about and /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 sets Cache-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:
  1. The directory’s linked site, in .bunny/site.json
  2. sites.name in bunny.jsonc
  3. An interactive picker, which offers to link the directory
A run that cannot prompt reports an error instead. That is --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:
A 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.

Flags

Last modified on August 21, 2026