Bitdoze logo

How to Deploy Astro on a VPS with CloudPanel (2026 Guide)

Learn how to deploy Astro on a VPS with CloudPanel: build a static site, add SSL and Cloudflare CDN, automate rebuilds, and roll back safely on your server.

Dragos

19 min read

How to Deploy Astro on a VPS with CloudPanel (2026 Guide)

Deploying Astro on a VPS with CloudPanel gives you the one thing free platforms never do: full control. Your build, your cache headers, your rollback path, and no build-minute caps waiting to surprise you at the end of the month. Free hosting like Cloudflare Pages, Vercel, and Netlify works well until it doesn’t: bandwidth caps, build limits, or platform lock-in.

CloudPanel is a free hosting panel that runs on Nginx. It handles site management, SSL, and user isolation through a web UI. You deploy Astro as a Static HTML site and let CloudPanel serve the built files directly from Nginx. No Node.js process runs at runtime, which means a small, cheap VPS is enough. And since Astro 7 shipped in June 2026 with 15 to 61% faster builds (our Astro 7 build benchmark), rebuilds on that small box are quicker than they used to be.

DigitalOcean $100 Free Vultr $100 Free Hetzner €20 Free Hostinger VPS

VPS prices jumped across the board in 2026. If you’re rethinking a rented box, see what changed and when a mini PC wins.

Prerequisites

Before you start, here is what needs to already exist:

The stack this guide builds: Linux VPS + CloudPanel (Nginx) + NVM + Node 24 + Cloudflare in front. Everything else is optional.

Keep CloudPanel patched

CloudPanel’s changelog shows a real CVE history, including privilege-escalation fixes between 2022 and 2024. Current release is v2.5.4 (2026-07-01). Run the panel’s update routine after install and don’t leave it stale. You’re exposing an admin UI on a public IP.

You can also deploy Astro on a VPS with Coolify or EasyPanel if you prefer a different panel. For Docker-heavy stacks those are fine choices. For a pure static Astro site, CloudPanel is the simpler path.

If you want to monitor CPU, memory, and disk space on your server, check: How To Monitor Server and Docker Resources

Video walkthrough

The video shows an earlier CloudPanel UI

The walkthrough was recorded in 2023. The flow is the same, but the CloudPanel v2.5.x UI differs in a few places. The written steps below are current for CloudPanel v2.5.4 (2026-07-01). Trust those if the screenshots don’t match what you see.

Deploy Astro.js on VPS with CloudPanel

There are two approaches in CloudPanel: a Static HTML site (recommended for pure static output) or a Node.js site. The Static HTML approach is simpler and cheaper to run: CloudPanel serves the built files directly from Nginx with no Node.js process at runtime. Here’s the full sequence: site, DNS, SSL, Node for building, project, build, then automation.

Step 1: Add a static HTML site in CloudPanel

  1. Log in to your CloudPanel admin panel.
  2. Go to Sites > Add Site > Create a Static HTML Site. (CloudPanel’s current docs for all site types live at cloudpanel.io/docs/v2/frontend-area/add-site/.)
  3. Enter your domain name and create the site user credentials.
  4. After the site is created, edit it and change the Root Directory to point to the dist directory:
text
htdocs/www.yourdomain.com/dist

Astro outputs its production build into dist/ by default (outDir: './dist'). CloudPanel needs to serve from that directory, not the project root.

Always serve from dist/, never the project root

Serving the project root would expose your .git/ directory, node_modules/, source files, and .env through Nginx. Point the root directory at dist/ and nothing else is reachable. If you get a 403 or 404 after a “successful” build, this setting is the first thing to check.

CloudPanel site settings with the Root Directory set to htdocs/www.yourdomain.com/dist for an Astro build

CloudPanel also creates the www↔non-www redirect and forces HTTP→HTTPS for you. You don’t need to hand-write those rules.

Step 2: Point DNS to your VPS

Add an A record in your DNS provider pointing your domain to the VPS IP address. If you use Cloudflare, add the A record there and enable the proxy for CDN benefits and DDoS protection.

bash
dig +short www.yourdomain.com
# With Cloudflare proxy on, you'll see Cloudflare IPs (that's expected)
# With proxy off (DNS only), you should see your VPS IP
Cloudflare DNS A record pointing the domain to the VPS IP with Cloudflare proxy enabled

Set Cloudflare SSL/TLS to Full (strict)

With a Let’s Encrypt certificate on the origin (next step) and Cloudflare in front, set SSL/TLS mode to Full (strict) in the Cloudflare dashboard. The default Flexible mode makes Cloudflare talk HTTP to your origin while serving HTTPS to visitors. That causes redirect loops with Nginx’s forced-HTTPS rules, or 522/525 errors. Full (strict) verifies the origin cert and the loop goes away.

Step 3: Generate a Let’s Encrypt SSL certificate

In CloudPanel, go to your site’s SSL/TLS section and click New Let’s Encrypt Certificate. CloudPanel handles the certificate generation and Nginx configuration automatically.

Verify it before moving on:

bash
curl -I https://www.yourdomain.com
# Expect: HTTP/2 200 (the CloudPanel placeholder page is fine at this stage)

curl -I http://www.yourdomain.com
# Expect: 301 redirect to https://www.yourdomain.com

If you get a certificate warning in curl, the cert didn’t issue. Check that the DNS record from Step 2 is already resolving before retrying.

Step 4: Install Node.js 24 LTS with NVM

Astro needs Node.js to build the site. CloudPanel creates isolated Linux users per site, so install Node.js under the site user, not as root. SSH into your VPS and switch to the site user:

bash
ssh root@your-server-ip
sudo su - www.yourdomain.com

Install NVM (Node Version Manager). v0.40.8 is current as of September 2026:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
source ~/.bashrc

Install Node.js 24, the active LTS line:

bash
nvm install 24
nvm alias default 24

Verify the installation:

bash
node -v
# Should output v24.x.x

nvm ls
# Should show v24.x.x as default

Astro 7 needs Node >= 22.12.0

The current Astro release line (7.x, 7.3.5 on npm as of Oct 2026) requires node >=22.12.0. Node 24 satisfies that and is supported until 2028-04-30. Node 22 still works (maintenance LTS until 2027-04-30). nvm install 22 grabs the newest 22.x, which clears the 22.12.0 floor, but 24 buys you an extra year of support. Install 24 unless you have a reason not to.

Step 5: Clone and set up your Astro project

Remove the default CloudPanel placeholder files and get your project in place. The directory name must match what CloudPanel expects (www.yourdomain.com).

Clone an existing repo

bash
cd htdocs
rm -rf www.yourdomain.com
git clone [email protected]:your-username/your-astro-repo.git www.yourdomain.com
cd www.yourdomain.com
npm install

For a private repo, [email protected]:... needs an SSH key for the site user. Create one and add it to the repo as a read-only deploy key:

bash
ssh-keygen -t ed25519 -C "deploy-www.yourdomain.com"
cat ~/.ssh/id_ed25519.pub
# Copy the output, then in GitHub: repo Settings > Deploy keys > Add deploy key
# Paste it and leave "Allow write access" unchecked

If you need a deeper walkthrough of SSH keys with GitHub, see how to link GitHub with an SSH key.

Scaffold a new project

bash
cd htdocs
rm -rf www.yourdomain.com
npm create astro@latest www.yourdomain.com
cd www.yourdomain.com
npm install

This still scaffolds Astro 7 projects correctly. For a ready-made blog theme, check Bitdoze Astro Theme or AstroWind.

Permission denied (publickey)?

That error on git clone or git pull means the site user has no SSH key registered with GitHub. The deploy-key steps above are the fix. Test with ssh -T [email protected] as the site user. You should see “Hi username/repo! You’ve successfully authenticated”.

Step 6: Build the Astro static site

Once your site is configured and content is in place, build the production version:

bash
npm run build

This generates the static files in dist/. Since CloudPanel’s root directory already points at dist/, the site goes live the moment the build finishes.

Verify:

bash
curl -I https://www.yourdomain.com
# Expect: HTTP/2 200 and content-type: text/html

curl -I https://www.yourdomain.com/_astro/*.css
# Expect: 200. Astro emits hashed assets under /_astro/

OOM-killed on a 1 GB VPS?

npm install and Astro builds can eat more than 1 GB of RAM, and the kernel OOM killer will happily abort the build. Either resize to 2 GB or add 1 to 2 GB of swap first:

bash
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Astro 7’s faster builds (15 to 61%, our Astro 7 build benchmark) shorten the window but don’t remove the memory floor. For heavy sites, see Astro build speed optimization.

Rebuilding an old Astro 4/5/6 site on Astro 7?

Astro 7’s new Rust compiler no longer corrects markup silently. Unclosed tags and unterminated attributes are now hard build errors, and whitespace between inline elements collapses JSX-style (newlines no longer render as a space; use { ' ' } where you need one). If you hit a wall of markup errors after npm install pulled Astro 7, that’s why. Upgrade path: npx @astrojs/upgrade, then fix the reported lines.

Step 7: Automate rebuilds and deployments

Pulling and building by hand gets old on the third content push. The whole pipeline is small:

Astro deploy pipeline: git push triggers a deploy script that runs git pull, npm ci, and npm run build into dist, which CloudPanel Nginx serves over HTTPS through Cloudflare to the visitor

Write a minimal deploy script as the site user, at ~/deploy.sh:

bash
#!/usr/bin/env bash
set -euo pipefail

cd ~/htdocs/www.yourdomain.com
git pull
npm ci
npm run build
bash
chmod +x ~/deploy.sh

Use npm ci instead of npm install. It installs exactly what’s in package-lock.json, so a rebuild tomorrow behaves like the rebuild today. Now pick a trigger:

CloudPanel cron

In CloudPanel, go to Cron Jobs for the site user and run the script on a schedule, for example every 5 minutes:

bash
*/5 * * * * /home/www.yourdomain.com/deploy.sh >> /home/www.yourdomain.com/deploy.log 2>&1

To avoid pointless rebuilds, add a fetch check at the top of the script:

bash
cd ~/htdocs/www.yourdomain.com
git fetch -q
[ "$(git rev-parse HEAD)" = "$(git rev-parse @{u})" ] && exit 0

The build runs only when the remote has new commits. Crude, reliable, no inbound ports.

GitHub webhook

Add a webhook in GitHub (repo Settings > Webhooks): payload URL pointing at a tiny listener on the VPS that runs deploy.sh, content type application/json, event “Just the push event”.

The listener can be a 20-line Python service or a shell script behind a throwaway endpoint. Point it at a path that is not your website’s vhost, and put a shared secret on it. Otherwise anyone who finds the URL can trigger builds on your server. Deploy keys are read-only; the webhook listener is the only inbound surface you add.

DPLOY

CloudPanel ships a Git-native deploy tool, DPLOY. Push to a branch, DPLOY pulls and runs your build command. It’s the “native” option if you want the panel to own the deploy flow instead of cron plus a shell script.

Check the docs page for the current install steps. The docs are still live and match v2.

Whichever trigger you use: the build output lands in cron logs or the webhook’s journal. When a deploy “silently” doesn’t happen, that log is the first place to look. The script is also where the rollback safeguard hooks in (covered next).

DigitalOcean $100 Free Vultr $100 Free Hetzner €20 Free Hostinger VPS

Roll back a failed deploy

npm run build empties dist/ before writing new output. If the build fails midway (markup error, OOM kill, broken dependency), you can be left serving nothing. The fix is boring: keep the previous good build around until the new one is proven.

Rollback swap diagram: before a rebuild move dist to dist.last-known-good, run the build which empties dist first and can fail, then on success drop the backup or on failure move the backup back so the site keeps serving

Extend the deploy script with the swap:

bash
#!/usr/bin/env bash
set -euo pipefail

cd ~/htdocs/www.yourdomain.com
git pull
npm ci

# Keep the previous build until the new one is proven
if [ -d dist ]; then
  rm -rf dist.last-known-good
  mv dist dist.last-known-good
fi

if npm run build && [ -f dist/index.html ]; then
  rm -rf dist.last-known-good
else
  echo "Build failed or produced no index.html, restoring previous dist" >&2
  if [ -d dist.last-known-good ]; then
    rm -rf dist
    mv dist.last-known-good dist
  fi
  exit 1
fi

The [ -f dist/index.html ] check catches the worst case: a build that exits 0 but emits an empty or wrong output tree. If it fails, the backup is swapped back and Nginx keeps serving the previous version.

This is a “last known good static output” strategy, not versioned deploys. For a solo static site it’s enough: restore time is a mv, and the only state that matters (the git repo) is elsewhere.

Verify your deployment and set cache headers

Run this checklist after the first deploy and after every rebuild. It takes 30 seconds and catches most of what breaks:

Then caching. Astro emits hashed files under /_astro/, so those are safe to cache hard: Cache-Control: public, max-age=31536000, immutable. HTML should stay short-lived, otherwise visitors keep seeing the previous deploy after you rebuild.

CloudPanel’s static vhost template handles basic serving, but if you want explicit, predictable headers, add them in the site’s Vhost editor (CloudPanel vhost docs):

nginx
location /_astro/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

Leave HTML to default handling, or set a short Cache-Control (60 seconds is plenty). On the Cloudflare side, set Browser Cache TTL to “Respect Existing Headers” so the origin wins. That way a rebuild shows up without you needing to purge.

Deploys not showing up?

Two usual suspects: a long HTML cache TTL (fix above), or Cloudflare’s edge still holding the old page. Purge the Cloudflare cache for the HTML pages after a deploy if you need it live immediately. The /_astro/ assets don’t need purging. Their filenames change when their content changes.

After the site is live, watch it: monitor uptime with Beszel and Uptime Kuma and track server resources. A static site can still go down: cert expiry, disk full, Nginx misconfig on the next edit.

If you’d rather not put Cloudflare in front, Bunny.net works as a drop-in edge CDN on the same origin. Pull zone pointed at your VPS, same /_astro/ immutable caching story.

Troubleshooting common failures

Most of these are 2-minute fixes

Each entry below is error → cause → fix. The full walkthrough for each lives in the step linked inside.

git clone or git pull fails with Permission denied (publickey)

The site user has no SSH key that GitHub knows about. Generate one (ssh-keygen -t ed25519 as the site user), add the public key to the repo as a read-only deploy key, and test with ssh -T [email protected]. Full steps in Step 5. Common variant: you added the key to your personal GitHub account but the repo is in an organization that restricts deploy keys. Use a repo deploy key instead.

Build gets killed on a small VPS (npm install or astro build)

Out of memory. On 1 GB boxes the kernel OOM killer aborts the build with no useful Astro error. Fix: add 1 to 2 GB of swap (commands in Step 6) or resize the VPS to 2 GB. Check dmesg -T | grep -i oom to confirm it’s OOM and not something else. Astro 7’s faster builds shorten the window but don’t lower the memory floor.

Redirect loop or 522/525 errors behind Cloudflare

Cloudflare SSL/TLS mode is probably on Flexible, so Cloudflare talks HTTP to your origin while Nginx forces HTTPS. That’s an infinite redirect. Set SSL/TLS to Full (strict) with the Let’s Encrypt cert on the origin. Details in Step 2.

Hard build errors after upgrading to Astro 7

The new Rust compiler doesn’t fix markup silently anymore: unclosed tags and unterminated attributes are build errors, and inline whitespace behaves JSX-style. Fix the reported lines, or run npx @astrojs/upgrade and work through the list. Details in Step 6.

Site shows 404 or 403 after a successful build

The root directory isn’t pointing at dist/. In CloudPanel, edit the site and set Root Directory to htdocs/www.yourdomain.com/dist. If the directory is right and dist/index.html doesn’t exist, the build output went elsewhere: check outDir in astro.config.mjs. Serving the project root instead of dist/ “fixes” the 404 and exposes .git/. Don’t do it.

Deploy ran but visitors still see the old site

Cache. Either the browser is holding a long HTML TTL, or Cloudflare’s edge hasn’t expired it. Shorten HTML cache (see the verify section), purge Cloudflare after deploy if it must be instant, and confirm with curl -s https://www.yourdomain.com | grep -o '_astro/[^\"]*\.css' that new hashed asset names are being served.

Alternative: use CloudPanel’s Node.js site type

Static HTML site (recommended)

Serves dist/ directly from Nginx. No Node.js process at runtime.

  • Lowest memory footprint. A 1 to 2 GB VPS is plenty
  • Nothing to keep alive (no PM2, no process manager)
  • Rebuild with Node, serve with Nginx

Right default for every pure static Astro site.

Node.js site

CloudPanel manages the Node.js version via NVM and proxies to an app port.

  • Pick Node.js version 24 LTS (supported since CloudPanel v2.5.4, 2026-07-01; 22 is still selectable)
  • Set an app port (e.g. 3000) and run the app with a process manager
  • Needed only if you switch to SSR with an adapter later

Same Node version you’d install by hand in the static flow, just managed by the panel. The cost is a permanent Node process using RAM 24/7.

For a purely static site, use the Static HTML site type. You can always recreate the site as Node.js later if you go SSR. It’s a 10-minute change. If you’re still deciding on a panel overall, the best self-hosted server panels comparison covers the trade-offs.

Conclusions

Deploying Astro on a VPS with CloudPanel is the right move when you outgrow free hosting limits or want control over the box. The Static HTML site type is the simplest version of it: Nginx serves dist/, no Node process runs at runtime, a $5 to $10/month VPS is enough, and Astro 7’s faster builds make rebuilds on that small machine less painful. Add the deploy script, the dist.last-known-good swap, and the verify checklist, and you have a deploy path you can trust at 1 a.m.

One security recap: keep CloudPanel updated. The panel runs with real privileges on your server and the changelog shows the CVE history to prove it matters.

DigitalOcean $100 Free Vultr $100 Free Hetzner €20 Free Hostinger VPS

If you want a web panel that also works as a reverse proxy for Docker containers, check this course:

CloudPanel Setup Course

Where to go next: