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 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 build that renders pages per request is a different shape, and this command does not deploy one. bunny lab deploy astro deploys an Astro project as an Edge Script, with its client build in Bunny Storage.

Deploy this project

1

Log in

2

Run the command in your project

With no site linked yet, the command offers to create one:
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.

Deploy a directory

1

Deploy

The files go up, and the deploy becomes the live site. A site with no linked directory is created first, or picked.
2

Roll back, when you need to

Publishing points the router at another 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 stays there. Publishing only moves the router’s pointer, so any past deploy goes live again with nothing re-uploaded. A deploy serves from the site’s root, not under a path prefix. So a client-side router and a root-absolute asset work with no configuration. 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

The 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.

_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. 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 25, 2026