How to Deploy Your Static Astro Website to Bunny.net
Learn how to deploy your Astro static website to Bunny.net CDN with edge storage. Includes a deploy script, GitHub Actions CI/CD, caching setup, and Bunny Shield WAF configuration.
You built an Astro site. It’s fast, it’s static, and now you need somewhere to host it that won’t cost a fortune or require you to maintain a server.
I run my own Astro site on Bunny.net, and after months of using it in production I can say the setup is straightforward once you know the steps. Bunny gives you edge storage with global replication and a CDN with 119+ points of presence. You upload your built files, Bunny serves them from the nearest edge location to every visitor. No VPS to patch, no Nginx config to debug, no Docker container to babysit.
I wrote a full Bunny.net review covering pricing and features in detail. This guide does one thing: walk you through getting your Astro static site deployed and live on Bunny.net.
If you don’t have an Astro site yet, I covered how to build a free blog with Astro using the Bitdoze Astro Theme. The same setup applies here, just swap Cloudflare Pages for Bunny.net hosting.
Try Bunny.net free for 14 days
You can follow along with this guide without spending anything. Sign up at Bunny.net with no credit card required and get a full 14-day free trial.
What you need before starting
- An Astro site configured for static output (or fork the Bitdoze Astro Theme to follow along)
- A Bunny.net account (free 14-day trial, no credit card)
- Node.js installed on your machine
- A domain name (optional, but recommended for production)
Why Bunny.net for static sites
A quick aside on why this is worth doing before we get into the setup.
- No server maintenance. Upload files, Bunny serves them. No OS updates, no security patches.
- Edge storage replication. Your site files live in multiple regions worldwide, not a single data center.
- Perma-Cache stores content permanently on edge servers so visitors almost always get a cache hit.
- Storage starts at $0.01/GB, bandwidth at $0.01/GB in EU/NA. Most blogs run for under $1/month.
- Let’s Encrypt certificates are provisioned automatically. No certbot, no renewal cron jobs.
- Custom domains with full DNS control. Bring your own domain and Bunny handles the rest.
Here’s how Bunny.net stacks up against other popular static hosting options:
| Feature | Bunny.net | Cloudflare Pages | Netlify | Vercel |
|---|---|---|---|---|
| Bandwidth price (EU/NA) | $0.01/GB | Free (limited) | $0.10/GB | $0.15/GB |
| Storage price | $0.01/GB | Free (limited) | Included | Included |
| Edge locations | 119+ | 300+ | ~100 | ~100 |
| Custom domain | Free | Free | Free | Free |
| Free tier | 14-day trial | Yes (generous) | 100GB/mo | 100GB/mo |
| WAF / DDoS protection | Bunny Shield (free tier) | Included | Add-on | Add-on |
| Min. monthly cost | $1 | $0 | $0 | $0 |
Cloudflare Pages has the better free tier if cost is your only concern. But if you want edge storage replication, predictable pricing at scale, or you already use Bunny for CDN or video, hosting your Astro site there keeps everything in one dashboard. I switched from Cloudflare Pages to Bunny for exactly that reason.
Step 1: Build your Astro site
Make sure your Astro project is configured for static output. Open astro.config.mjs and confirm it looks something like this:
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://yourdomain.com',
// output: 'static' is the default, so you may not see this line at all
});
Astro uses static output by default, so unless you explicitly changed it to server or hybrid, you’re already set.
Build the site:
npm run build
This generates a dist/ folder containing your complete static site — HTML files, CSS, JavaScript, images, everything. You can preview it locally to verify everything looks right:
npm run preview
The dist/ folder is what we’ll deploy to Bunny.net.
Step 2: Set up Bunny.net infrastructure
You need two things on the Bunny.net side: a storage zone (where your files live) and a pull zone (the CDN layer that serves them to visitors). Neither takes more than a couple of minutes.
Create a storage zone

- Log into your Bunny.net dashboard
- Go to Storage in the left sidebar
- Click Add Storage Zone
- Fill in the details:
- Name: Something like
my-astro-site(this becomes part of the storage URL) - Main Region: Pick the region closest to your primary audience (e.g.,
DEfor Europe,NYfor US East) - Replication Regions: Add regions where you want copies of your files (e.g., add
NYandSYDif you have visitors in the US and Australia) - Tier: Edge tier (SSD storage) is worth the tiny price difference for better performance
- Name: Something like
Choose regions carefully
You cannot change the main region or remove replication regions after creating a storage zone. You can add replication regions later, but start with your primary audience’s region and add more as needed.
Create a pull zone

The pull zone is the CDN configuration that sits in front of your storage zone and serves files to visitors.
- In your newly created storage zone, go to Connected Pull Zones
- Click Connect Pull Zone
- Configure it:
- Name: Something like
my-astro-site(this becomesmy-astro-site.b-cdn.net) - Zones: Select the geographic zones you want to serve traffic from (EU, NA, Asia, etc.)
- Name: Something like
That’s the basic infrastructure. Your pull zone hostname (my-astro-site.b-cdn.net) is already live and will serve whatever is in your storage zone. Right now that’s nothing, so let’s fix that.
Step 3: Deploy your site
The deployment uploads files from your dist/ folder to Bunny Storage via the Bunny API, then purges the CDN cache. You can do this from your local machine or automate it with GitHub Actions.
Get your credentials
You need four values from the Bunny dashboard:
- Storage Zone Name: The name you chose when creating the storage zone (e.g.,
my-astro-site) - Storage Password: Go to Storage → your storage zone → FTP & API Access → copy the Password
- Pull Zone ID: Go to Pull Zones → your pull zone → check the URL, it contains the pull zone ID (or find it in the pull zone settings)
- Account API Key: Go to Account Settings → copy the API Key

Create a .env file in your project root (add it to .gitignore so you don’t commit secrets):
# .env
BUNNY_STORAGE_ZONE=your-storage-zone-name
BUNNY_STORAGE_PASSWORD=your-storage-zone-password
BUNNY_PULL_ZONE_ID=your-pull-zone-id
BUNNY_API_KEY=your-account-api-keyThen create the deploy script. Save it as deploy.sh in your project root:
#!/bin/bash
set -euo pipefail
# Load environment variables from .env
if [ ! -f .env ]; then
echo "Error: .env file not found. Copy .env.example to .env and fill in your credentials."
exit 1
fi
set -a
source .env
set +a
# Validate required variables
for var in BUNNY_STORAGE_ZONE BUNNY_STORAGE_PASSWORD BUNNY_PULL_ZONE_ID BUNNY_API_KEY; do
if [ -z "${!var:-}" ]; then
echo "Error: $var is not set in .env"
exit 1
fi
done
DIST_DIR="dist"
# Build the site
echo "Building Astro site..."
npm run build
# Collect file list into a temp file to avoid pipe subshell issues
FILE_LIST=$(mktemp)
trap 'rm -f "$FILE_LIST"' EXIT
find "$DIST_DIR" -type f > "$FILE_LIST"
total=$(wc -l < "$FILE_LIST")
echo "Uploading $total files to Bunny Storage..."
failed=0
count=0
while IFS= read -r file; do
count=$((count + 1))
remote_path="${file#$DIST_DIR/}"
encoded_path=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$remote_path'))")
printf " [%d/%d] Uploading: %s" "$count" "$total" "$remote_path"
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
"https://storage.bunnycdn.com/$BUNNY_STORAGE_ZONE/$encoded_path" \
-H "AccessKey: $BUNNY_STORAGE_PASSWORD" \
--data-binary "@$file" || true)
if [ "$http_code" -eq 201 ] || [ "$http_code" -eq 200 ]; then
echo " -> OK"
else
echo " -> FAILED (HTTP $http_code)"
failed=$((failed + 1))
fi
done < "$FILE_LIST"
echo "Upload complete: $((count - failed))/$count succeeded."
if [ "$failed" -gt 0 ]; then
echo "Warning: $failed file(s) failed to upload."
fi
# Purge CDN cache
echo "Purging CDN cache for pull zone $BUNNY_PULL_ZONE_ID..."
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"https://api.bunny.net/pullzone/$BUNNY_PULL_ZONE_ID/purgeCache" \
-H "AccessKey: $BUNNY_API_KEY" || true)
if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 204 ]; then
echo "Cache purged successfully."
else
echo "Warning: cache purge returned HTTP $http_code"
fi
echo "Deployment complete!"Make it executable and run it:
chmod +x deploy.sh
./deploy.shThe script builds your Astro site, uploads every file from dist/ to Bunny Storage using the storage API, then purges the CDN cache so visitors see the fresh version immediately.
To deploy automatically on every push to main, use the same approach in a GitHub Actions workflow.
Set up secrets
Go to your GitHub repository → Settings → Secrets and variables → Actions and add these secrets:
| Secret name | Value |
|---|---|
BUNNY_STORAGE_ZONE | Your storage zone name |
BUNNY_STORAGE_PASSWORD | Your storage zone password (from FTP & API Access) |
BUNNY_PULL_ZONE_ID | Your pull zone ID |
BUNNY_API_KEY | Your account API key |
Create the workflow
Create .github/workflows/deploy.yml:
name: Deploy to Bunny.net
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Upload to Bunny Storage
env:
BUNNY_STORAGE_ZONE: ${{ secrets.BUNNY_STORAGE_ZONE }}
BUNNY_STORAGE_PASSWORD: ${{ secrets.BUNNY_STORAGE_PASSWORD }}
run: |
DIST_DIR="dist"
total=$(find "$DIST_DIR" -type f | wc -l)
echo "Uploading $total files to Bunny Storage..."
count=0
failed=0
while IFS= read -r file; do
count=$((count + 1))
remote_path="${file#$DIST_DIR/}"
encoded_path=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$remote_path'))")
printf " [%d/%d] Uploading: %s\n" "$count" "$total" "$remote_path"
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
"https://storage.bunnycdn.com/$BUNNY_STORAGE_ZONE/$encoded_path" \
-H "AccessKey: $BUNNY_STORAGE_PASSWORD" \
--data-binary "@$file" || true)
if [ "$http_code" -ne 201 ] && [ "$http_code" -ne 200 ]; then
echo " -> FAILED (HTTP $http_code)"
failed=$((failed + 1))
fi
done < <(find "$DIST_DIR" -type f)
echo "Upload complete: $((count - failed))/$count succeeded."
if [ "$failed" -gt 0 ]; then
echo "Error: $failed file(s) failed to upload."
exit 1
fi
- name: Purge CDN cache
env:
BUNNY_PULL_ZONE_ID: ${{ secrets.BUNNY_PULL_ZONE_ID }}
BUNNY_API_KEY: ${{ secrets.BUNNY_API_KEY }}
run: |
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"https://api.bunny.net/pullzone/$BUNNY_PULL_ZONE_ID/purgeCache" \
-H "AccessKey: $BUNNY_API_KEY")
if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 204 ]; then
echo "Cache purged successfully."
else
echo "Warning: cache purge returned HTTP $http_code"
fiEvery push to main now triggers: install dependencies, build the Astro site, upload all files to Bunny Storage, purge the CDN cache. Fully automated.
Step 4: Configure your custom domain
Your site is already accessible at your-pull-zone.b-cdn.net, but you probably want your own domain. Here’s how.
Add the hostname in Bunny
- Go to Pull Zones → your pull zone → General
- Scroll to Hostnames
- Click Add Custom Hostname
- Enter your domain (e.g.,
www.yourdomain.comoryourdomain.com) - Click Add
Bunny generates an SSL certificate automatically (usually within a few minutes).
Configure DNS at your registrar
How you configure DNS depends on whether you’re using a subdomain or the root domain.
For a subdomain (e.g., www.yourdomain.com):
Add a CNAME record:
| Type | Name | Value |
|---|---|---|
| CNAME | www | your-pull-zone.b-cdn.net |
CNAME records are the standard approach. They route traffic through Bunny’s Anycast network efficiently.
For a root domain (e.g., yourdomain.com):
Add an ANAME (also called ALIAS) record:
| Type | Name | Value |
|---|---|---|
| ANAME / ALIAS | @ | your-pull-zone.b-cdn.net |
Not all DNS providers support ANAME records. Cloudflare, DNSMadeEasy, and Bunny’s own DNS do. If yours doesn’t, use a subdomain like www with a CNAME instead, then set up a redirect from the root domain to www.
CNAME vs ANAME for root domains
ANAME records resolve to an IP address at the DNS level, which can slightly reduce routing optimization compared to CNAME. If performance matters more than a clean root domain URL, use www with a CNAME record instead. Bunny has a detailed writeup on how ANAME records affect CDN routing if you want the technical details.
Verify and enforce SSL
- Wait for the SSL certificate status to change to Active (check the Hostnames section in your pull zone)
- Toggle Force SSL to redirect all HTTP traffic to HTTPS
- Verify by visiting
https://yourdomain.comin a browser
DNS propagation
DNS changes can take anywhere from a few minutes to 48 hours to propagate globally, though most updates show up within 15-30 minutes. You can check propagation status with tools like dig or online DNS checkers.
Step 5: Configure caching
Bunny has three caching features worth understanding for a static Astro site. They work together in layers.
Smart Cache
Smart Cache is enabled by default on pull zones accelerated by Bunny DNS. It decides what gets cached and what passes through to the origin on every request.
For static sites, Smart Cache caches everything with a recognized static file extension (images, fonts, CSS, JS, PDFs, etc.) and never caches text/html, application/json, or application/xml MIME types. This is the right behavior for most Astro sites since your HTML pages will be fetched fresh while assets get cached.
If you need to cache HTML pages (Bunny excludes them by default), create an Edge Rule with the Override Cache Time action targeting your HTML files.
Vary Cache
Vary Cache lets Bunny store different versions of the same URL based on factors like browser capabilities, device type, or location. The relevant options for a static Astro site:
| Setting | What it does |
|---|---|
| WebP support | Serves WebP images to browsers that support them, original format to the rest |
| AVIF support | Same idea, but for the AVIF format |
| URL Query String | Treats different query strings as separate cached files |
Watch your cache cardinality
Each Vary setting multiplies the number of cached versions per URL. WebP + AVIF + Mobile/Desktop creates 2 x 2 x 2 = 8 cached versions of each file. Only enable settings you actually need, or your cache hit rate drops.
Perma-Cache
Perma-Cache is a secondary permanent cache layer between the CDN and your origin. When a cache miss occurs on the CDN edge, Bunny checks Perma-Cache storage first before hitting your origin. Files that get fetched from the origin are stored permanently in Perma-Cache in the background.
This is different from regular CDN caching, which expires based on time or available space. Perma-Cache files stick around indefinitely.
Perma-Cache vs Storage Zone origin
If your pull zone is directly connected to a storage zone as its origin (which is the setup in this guide), Perma-Cache is not available because your content is already hosted on Bunny storage. Perma-Cache is useful when your origin is an external server (a VPS, for example) and you want Bunny to cache origin responses permanently.
To enable Perma-Cache:
- Create a separate storage zone (different from the one holding your site files)
- Go to Pull Zones → your pull zone → Caching → Perma-Cache
- Select the storage zone from the dropdown
- Save configuration
Step 6: Add security with Bunny Shield
Bunny Shield is Bunny’s WAF (Web Application Firewall) and DDoS protection service. It sits in front of your pull zone and filters malicious traffic before it reaches your site. There’s a free tier that covers the basics.
What the free tier gives you
The free Shield tier includes 71 built-in WAF rules and DDoS protection. It blocks common attacks automatically:
- SQL injection attempts in query parameters and forms
- Cross-site scripting (XSS) payloads
- Remote file inclusion (RFI) attacks
- Other OWASP Top 10 threats
For a static Astro site, Shield is mostly defense against DDoS and bot traffic since there’s no server-side code to exploit. But it still reduces noise in your logs and can block scrapers or abuse patterns.
Enable Shield
- Go to Shield in the Bunny dashboard
- Create a new Shield Zone
- Connect it to your pull zone
- The free tier activates automatically
Optional: rate limiting and custom WAF rules
The paid tiers ($9.50/month for Advanced, $99/month for Business) add:
- Custom WAF rules (10 on Advanced, 25 on Business)
- Advanced bot detection
- Rate limiting per IP or path
For a static blog, the free tier is usually enough. Rate limiting becomes relevant if you notice specific IPs hammering your site or if you want to block traffic from certain countries.
Set up Edge Rules for security headers
Edge Rules let you add custom headers to responses. Adding security headers is a quick win regardless of whether you use Shield:
- Go to Pull Zones → your pull zone → Edge Rules
- Create rules to add headers:
| Header | Value |
|---|---|
| X-Content-Type-Options | nosniff |
| X-Frame-Options | SAMEORIGIN |
| X-XSS-Protection | 1; mode=block |
| Referrer-Policy | strict-origin-when-cross-origin |
Cost breakdown
What does this actually cost? Here’s a realistic scenario for a typical Astro blog:
| Resource | Monthly usage | Cost |
|---|---|---|
| Edge storage | 500 MB | ~$0.01 |
| CDN bandwidth (EU/NA) | 25 GB | ~$0.25 |
| Shield (free tier) | - | $0.00 |
| Total | ~$0.26/month |
Under $4/year for a blog with moderate traffic, including DDoS protection and WAF. Even at 100 GB of monthly bandwidth you’re looking at roughly $1/month.
Bunny has a $1 monthly minimum, so very low-traffic sites still pay $1. But that $1 covers up to 100 GB of EU/NA bandwidth, which is more than most blogs use in a month.
Troubleshooting
Site returns 404 errors
Check that your files are uploaded to the root of your storage zone, not inside a subfolder. The dist/ folder contents (not the folder itself) should be at the top level. With the deploy script, this is handled by the path stripping logic (remote_path="${file#$DIST_DIR/}"). If you uploaded manually, make sure you uploaded the contents of dist/, not the dist/ directory itself.
Also verify your pull zone is connected to the correct storage zone (Pull Zones → your zone → General → Origin Type should show “StorageZone” with the right zone selected).
CSS and JavaScript not loading
This is usually a MIME type issue. Bunny.net should detect and serve correct MIME types automatically, but if something is off:
- Check the pull zone’s Routing settings
- Make sure the Enable Static File Processing option is enabled if available
- Verify that your
dist/folder contains the referenced assets with correct file extensions
If you’re using Astro’s default build output, CSS and JS files are placed in dist/_astro/ with hashed filenames. As long as the full dist/ folder was uploaded, these should work.
Changes not visible after deployment
The CDN is serving cached content. Purge the cache:
- From the dashboard: Pull Zones → your zone → Purge Cache → Purge All Files
- The deploy script handles this automatically after each upload
Cache purges propagate within seconds on Bunny’s network. If you’re using Perma-Cache, note that a full pull zone purge doesn’t delete Perma-Cache files. It switches to a new directory structure instead.
SSL certificate not provisioning
SSL certificates are provisioned automatically when you add a custom hostname, but they require the DNS record to be pointing to Bunny first. Verify:
- Your CNAME or ANAME record is correctly configured at your DNS provider
- DNS has propagated (use
dig yourdomain.comto check) - Wait a few minutes after DNS propagation for the certificate to be issued
If it’s been over 30 minutes and the certificate is still pending, remove the hostname from Bunny and re-add it.
Deploy script fails with authentication errors
Double-check your credentials:
- Storage Password: This is the password from the storage zone’s FTP & API Access page, not your account password
- API Key: This is the key from Account Settings, not the storage password
- Pull Zone ID: This is a numeric ID, not the pull zone name. You can find it in the pull zone settings or the dashboard URL
Also make sure your .env file doesn’t have trailing whitespace or quotes around the values.
Frequently asked questions
Can I use this with other static site generators?
Yes. The same setup works with Hugo, Next.js (static export), Gatsby, 11ty, Jekyll, or any tool that outputs a folder of static files. The only Astro-specific step is the build command (npm run build producing a dist/ folder). Replace that with your generator’s build command and output directory, and everything else stays the same.
Why use the deploy script instead of Bunny Launcher?
Bunny Launcher is a third-party tool that abstracts away the Bunny API. The deploy script in this guide calls the Bunny Storage and CDN APIs directly with curl, so you can see exactly what’s happening and adapt it to any environment. It works in GitHub Actions, GitLab CI, or a local terminal without installing extra npm packages.
What about server-side rendering (SSR)?
This guide covers static sites only. Bunny.net’s edge storage and CDN serve pre-built files with no server-side runtime. If your Astro site uses SSR mode (output: 'server'), you need a hosting platform that runs Node.js (Vercel, Netlify, or a VPS). You can still put Bunny CDN in front of your SSR server for caching, but that’s a different setup.
Do I need Bunny Optimizer?
Not necessarily. Astro handles image optimization, CSS minification, and JavaScript bundling during the build process. Your dist/ folder is already optimized. Bunny Optimizer is worth considering if you serve images that weren’t processed during build (user uploads, dynamically referenced assets) or want on-the-fly WebP/AVIF conversion. For a standard Astro blog, it’s redundant.
How do I handle redirects?
Use Bunny’s Edge Rules to set up redirects. Go to Pull Zones → your zone → Edge Rules and create a redirect rule. Common patterns include redirecting yourdomain.com to www.yourdomain.com, redirecting old URLs after a site migration, or enforcing trailing slash consistency.
For Astro’s built-in redirects (configured in astro.config.mjs), those generate a _redirects file in the dist/ folder during build. Bunny doesn’t process this file, so you’d need to replicate those rules as Edge Rules in the Bunny dashboard.
Wrapping up
Your Astro site is now running on Bunny.net’s global CDN with edge storage, caching, and DDoS protection via Shield. Storage, CDN, SSL, WAF, and global delivery for under $1/month for most blogs.
If you run into issues, the troubleshooting section above covers the common ones. The GitHub Actions workflow from Step 3 is the logical next step if you want automatic deploys on every push to main.
Get Started with Bunny.net