How to Deploy Astro on Ubuntu 24.04 VPS: Static + SSR with Node Adapter, Nginx
Astro is the content-first web framework that produces zero JavaScript by default, ships islands of interactivity only where you ask for them, and lets you mix static pages with server-rendered routes in the same project. It powers marketing sites, blogs, documentation, e-commerce storefronts and increasingly full SaaS dashboards. In this tutorial you will deploy a real Astro 5 project on an Ubuntu 24.04 VPS, first as a pure static site served by Nginx, then as a hybrid application with server-rendered routes running under PM2 cluster mode behind an Nginx reverse proxy.
By the end you will have a production-grade setup that matches what a managed platform like Vercel gives you, but on infrastructure that you own, at a flat monthly price.
Looking for a platform to host Astro? The CloudCore Starter plan from vps-server.host gives you enough CPU, RAM, and NVMe storage to run an Astro site and its toolchain comfortably, with a static IPv4, unmetered bandwidth, and full root access.
Table of Contents
What is Astro?
Astro is a web framework designed around content. Its defining feature is the islands architecture: pages are rendered to HTML at build or request time with zero client-side JavaScript, and interactive UI components (React, Vue, Svelte, Solid, Preact, Lit) are hydrated only where explicitly marked with a client:* directive. The result is lighter pages, faster Core Web Vitals and much smaller JavaScript bundles than a traditional SPA framework.
Astro 5 introduced several features that matter for production deployments: the Content Layer API for type-safe content from Markdown, MDX, JSON, YAML or remote sources; Server Islands that let you stream slow dynamic fragments into an otherwise static page; stable view transitions for SPA-like navigation; and a unified Image service built on Sharp. The framework officially supports Node.js 18.20+, 20.3+, 22+ and 24+ on Linux.
Typical Astro workloads on a VPS include marketing sites, SaaS documentation portals, engineering blogs, JAMstack e-commerce storefronts using headless providers like Shopify or Medusa, and hybrid apps where most pages are static but a handful of routes (auth, checkout, preview) are server-rendered.
Static, SSR, or Hybrid: Choosing an Output Mode
Astro supports three output modes, configured via the output field in astro.config.mjs:
output: 'static'(default) -- Every page is pre-rendered to HTML at build time. Thedist/directory contains plain.html,.css,.jsand asset files that any static server can host. No Node.js runtime needed in production.output: 'server'-- Every page is server-rendered on each request. You must install an adapter (@astrojs/nodeon a VPS) and run a Node process in production.output: 'static'withprerender = falseon selected routes (hybrid) -- The default is static, but individual pages or endpoints opt into SSR by exportingexport const prerender = false. This gives you a mostly static site with a few dynamic routes, which is the right shape for the majority of real projects.
'hybrid' mode has been merged into 'static' with per-route prerender flags. Use 'server' only when the default for a route should be SSR.Why Self-Host Astro Instead of Using Vercel
Astro happily deploys to Vercel, Netlify, and Cloudflare Pages. For small marketing sites those platforms are effectively free. The calculus changes quickly once you have real traffic, larger builds, SSR routes or a team:
- Predictable flat price -- A VPS is a fixed monthly bill. No metering on requests, bandwidth, function invocations, image transformations, build minutes, or seat licenses. A 20 GB image cache that would cost double-digit dollars in transformations on a managed platform costs zero on your own disk.
- No cold starts -- SSR routes on serverless platforms cold-start after periods of inactivity, adding hundreds of milliseconds to the first request. A long-running Node process under PM2 on a VPS is always warm.
- Local database and internal services -- If your Astro app talks to Postgres, Redis or an internal API on the same VPS (or the same private network), you avoid the round-trip to a remote managed database and you avoid egress fees.
- Full runtime control -- Pick the Node.js major version, install system packages that Sharp or Playwright need, mount persistent volumes, run cron jobs, tail real log files.
- No vendor-specific adapters or edge runtime limits -- Plain
@astrojs/noderuns the same code you test locally. No "this npm package does not work on the edge runtime" surprises. - Data residency -- Pick an EU, US or other region once and keep all data there. Simplifies GDPR and procurement conversations.
Prerequisites
Before you begin you will need:
- An Ubuntu 24.04 LTS VPS with root or sudo access
- SSH access to the server
- A domain name whose DNS A record points to the server's public IPv4
- Node.js 20 LTS or newer (we install this in Step 2)
- Basic familiarity with the Linux command line
Recommended plan: CloudCore Starter>
The CloudCore Starter plan is a great fit for a typical Astro project:>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth
- 1 dedicated IPv4>
This comfortably hosts a static Astro site alongside a Node SSR process, PM2, Nginx and Certbot. For larger traffic or heavier SSR workloads (image processing, database-backed SSR) move up to CloudCore Advanced.
If Node.js or Nginx are not yet installed on your server, follow our companion guides first:
Connect to your server:ssh root@your-server-ipStep 1: Prepare the Ubuntu Server
Update the package index and upgrade existing packages.
sudo apt update && sudo apt upgrade -yInstall the minimum set of build tools. Astro itself is pure JavaScript, but Sharp (the image optimizer) may need to rebuild against system libvips on some kernels, and git is required for cloning your project.
sudo apt install -y build-essential git curl ca-certificates ufwConfigure a minimal firewall. We only expose SSH, HTTP and HTTPS. The Node SSR process will bind to 127.0.0.1 and stay private.
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw statusExpected output:
Status: active
To Action From -- ------ ---- OpenSSH ALLOW Anywhere Nginx Full ALLOW Anywhere
Create a non-root system user to own the app files. Never run your web app as root.
sudo adduser --disabled-password --gecos "" astro
sudo usermod -aG www-data astroStep 2: Install Node.js and PM2
Use the NodeSource distribution to install Node.js 20 LTS. Astro 5 requires Node 18.20+, 20.3+, 22+ or 24+; Node 20 LTS is the safest long-lived choice.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejsVerify the versions:
node -v
npm -vExpected output:
v20.18.1
10.8.2Install PM2 globally. PM2 is the process manager that will supervise the Astro SSR process in production.
sudo npm install -g pm2
pm2 -vEnable PM2's systemd integration so it starts on boot:
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u astro --hp /home/astroPM2 prints a command to run; it is already the one above, so nothing else is needed.
Step 3: Scaffold the Astro Project
Switch to the astro user for everything that follows. Running npm install as root leaves files owned by root and causes permission errors later.
sudo -iu astroFrom the astro user's home directory, scaffold a new project with the official create-astro CLI:
cd ~
npm create astro@latest app -- --template blog --typescript strict --install --no-gitThe flags ask for the blog template (any template works -- minimal, basics, starlight for docs), TypeScript in strict mode, automatic npm install, and no git init since we will deploy via git pull later if you prefer.
Expected final output:
astro Liftoff confirmed. Explore your project!
Enter your project directory using cd ./app Run npm run dev to start the dev server.
Move into the project and verify it builds:
cd ~/app
npm run buildExpected output (abbreviated):
> astro build
20:00:00 [types] Generated 0.10s 20:00:01 [content] Syncing content collections... 20:00:01 [build] Collecting build info... 20:00:02 [build] Completed in 1.45s. 20:00:02 [build] 4 page(s) built in 1.45s 20:00:02 [build] Complete!
The generated static output lives in ~/app/dist/.
Step 4: Deploy a Static Astro Site
For a purely static site, Nginx directly serves the dist/ directory. This is the fastest possible deployment: no Node process, no PM2, no upstream.
Exit back to your sudo-capable user:
exitInstall Nginx if it is not already installed:
sudo apt install -y nginxCreate an Nginx site for your domain. Replace example.com with your actual domain throughout.
sudo tee /etc/nginx/sites-available/astro-static > /dev/null <<'EOF' server { listen 80; listen [::]:80; server_name example.com www.example.com;root /home/astro/app/dist; index index.html;
# Astro emits clean URLs by default (/about/index.html) location / { try_files $uri $uri/ $uri.html =404; }
# Fingerprinted assets: cache aggressively location /_astro/ { access_log off; expires 1y; add_header Cache-Control "public, immutable"; }
# Images, fonts: long cache location ~* \.(?:jpg|jpeg|png|webp|avif|gif|svg|woff2|ico)$ { access_log off; expires 30d; add_header Cache-Control "public"; }
# Do not cache HTML: re-check every request location ~* \.html$ { expires -1; add_header Cache-Control "no-cache"; }
# Gzip responses gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; } EOF
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/astro-static /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxExpected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulVisit http://example.com in a browser; you should see the Astro blog homepage. We will add TLS in Step 7.
Step 5: Enable Hybrid/SSR with @astrojs/node
Now let's add server-side rendering for the routes that need it. Switch back to the astro user and install the Node adapter:
sudo -iu astro
cd ~/app
npx astro add nodeWhen the CLI asks, choose standalone mode. The command updates astro.config.mjs, installs @astrojs/node, and enables SSR.
Your astro.config.mjs should now resemble:
import { defineConfig } from 'astro/config'; import node from '@astrojs/node';
export default defineConfig({ output: 'static', adapter: node({ mode: 'standalone' }), site: 'https://example.com', image: { // Sharp is the default. Set quality/format defaults here. service: { entrypoint: 'astro/assets/services/sharp' }, }, });
Standalone vs Middleware Mode
mode: 'standalone'-- Astro builds its own Node HTTP server entry point atdist/server/entry.mjs. You run it directly withnode ./dist/server/entry.mjsor under PM2. This is the simplest mode on a VPS and the one we use below.mode: 'middleware'-- Astro exports an Express/Connect-compatible middleware that you mount inside your own Node server. Use this when you have an existing Express/Fastify app that needs to serve Astro routes alongside custom API routes.
Opt a Route into SSR
Keep the default output: 'static' and make individual routes dynamic with a named export. Create src/pages/api/time.ts:
import type { APIRoute } from 'astro';export const prerender = false;
export const GET: APIRoute = async () => { return new Response( JSON.stringify({ now: new Date().toISOString() }), { headers: { 'Content-Type': 'application/json' } } ); };
Or a full SSR page at src/pages/dashboard.astro:
---
export const prerender = false;
const user = Astro.cookies.get('session')?.value ?? 'guest';
<html>
<body>
<h1>Hello, {user}</h1>
<p>Rendered at {new Date().toISOString()}</p>
</body>
</html>Build for Production
npm run buildExpected output:
20:10:00 [build] output: "static"
20:10:00 [build] adapter: @astrojs/node
20:10:01 [build] Building static entrypoints...
20:10:02 [build] Building server entrypoints...
20:10:03 [build] server dist: dist/server/
20:10:03 [build] client dist: dist/client/
20:10:03 [build] Server built in 1.14s
20:10:03 [build] Complete!You now have two directories inside dist/:
dist/client/-- Static files (pre-rendered HTML, CSS, JS, images). Served directly by Nginx.dist/server/-- Node entry pointentry.mjsthat handles the SSR routes.
node ./dist/server/entry.mjsExpected output:
Server listening on http://0.0.0.0:4321Visit http://your-server-ip:4321/api/time from a second terminal with curl and confirm you get JSON. Stop the server with Ctrl+C.
Step 6: Run SSR Under PM2 Cluster Mode
Running node entry.mjs directly is fine for testing, but a production deployment needs a process manager that: restarts crashes, starts on boot, rotates logs, runs on every CPU core, and does zero-downtime reloads.
Configure the port via an environment variable and bind only to localhost so Nginx is the only thing clients reach:
cd ~/app
cat > ecosystem.config.cjs <<'EOF'
module.exports = {
apps: [
{
name: 'astro-ssr',
script: './dist/server/entry.mjs',
cwd: '/home/astro/app',
instances: 'max', // one worker per CPU core
exec_mode: 'cluster', // PM2 cluster: load-balances across workers
env: {
HOST: '127.0.0.1',
PORT: '4321',
NODE_ENV: 'production'
},
max_memory_restart: '512M',
merge_logs: true,
time: true,
out_file: '/home/astro/.pm2/logs/astro-ssr-out.log',
error_file: '/home/astro/.pm2/logs/astro-ssr-err.log'
}
]
};
EOFStart the app:
pm2 start ecosystem.config.cjs
pm2 save
pm2 statusExpected output:
┌────┬──────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────┬──────┐
│ id │ name │ mode │ version │ pid │ uptime │ ↺ │ status │ cpu │ mem │
├────┼──────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────┼──────┤
│ 0 │ astro-ssr │ cluster │ 5.x.x │ 12345 │ 5s │ 0 │ online │ 2% │ 72mb │
│ 1 │ astro-ssr │ cluster │ 5.x.x │ 12346 │ 5s │ 0 │ online │ 2% │ 72mb │
└────┴──────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────┴──────┘Verify the SSR route locally:
curl http://127.0.0.1:4321/api/timeExpected output:
{"now":"2026-04-16T20:30:00.000Z"}Useful PM2 commands:
pm2 reload astro-ssr-- Zero-downtime reload (rolls workers one by one). Run this after everynpm run build.pm2 logs astro-ssr-- Stream live logs.pm2 monit-- Live CPU/memory dashboard per worker.pm2 stop astro-ssr/pm2 delete astro-ssr-- Stop or remove.
Step 7: Configure Nginx and TLS
Now we wire Nginx in front of the Node SSR process, serving static assets directly from disk and proxying only the dynamic routes to PM2. Exit to your sudo user:
exitReplace the earlier static-only config:
sudo tee /etc/nginx/sites-available/astro-static > /dev/null <<'EOF' upstream astro_ssr { server 127.0.0.1:4321; keepalive 32; }server { listen 80; listen [::]:80; server_name example.com www.example.com;
root /home/astro/app/dist/client; index index.html;
# Security headers add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always;
# Fingerprinted Astro assets location /_astro/ { access_log off; expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; }
# Static assets location ~* \.(?:jpg|jpeg|png|webp|avif|gif|svg|woff2|ico|css|js)$ { access_log off; expires 30d; add_header Cache-Control "public"; try_files $uri @ssr; }
# Try static HTML first, fall back to SSR location / { try_files $uri $uri/ $uri.html @ssr; }
# Named location for the SSR upstream location @ssr { proxy_pass http://astro_ssr; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; proxy_buffering off; }
gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; } EOF
sudo nginx -t sudo systemctl reload nginx
The try_files chain is the key: Nginx first attempts to serve a pre-rendered .html from disk (fast path for static pages), and only falls through to the Node SSR upstream for routes that do not exist statically. This gives you the best of both worlds -- static performance for 95% of traffic, dynamic rendering only where needed.
Add HTTPS with Certbot
Install Certbot and request a Let's Encrypt certificate:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com --redirect --agree-tos -m [email protected] --no-eff-emailCertbot rewrites the config to listen on 443, adds the certificate paths, and installs a 301 redirect from HTTP to HTTPS. Test automatic renewal:
sudo certbot renew --dry-runYour site is now live at https://example.com with TLS, HTTP/2, static serving and SSR fallback.
Step 8: Content Collections, View Transitions and Sharp
Content Collections
Astro 5's Content Layer API turns Markdown, MDX, JSON or YAML into strongly-typed data you can query anywhere in your templates. Define a collection in src/content.config.ts:
import { defineCollection, z } from 'astro:content'; import { glob } from 'astro/loaders';const blog = defineCollection({ loader: glob({ pattern: '*/.md', base: './src/content/blog' }), schema: z.object({ title: z.string(), description: z.string(), pubDate: z.coerce.date(), heroImage: z.string().optional(), draft: z.boolean().default(false), }), });
export const collections = { blog };
Query and render posts in src/pages/blog/[...slug].astro:
---
import { getCollection, render } from 'astro:content';export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
<article>
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toDateString()}
</time>
<Content />
</article>Every frontmatter field is validated against the Zod schema at build time, so a missing or mistyped field breaks the build rather than silently shipping a broken page.
View Transitions
Add SPA-like page transitions site-wide by importing the ClientRouter component in your base layout:
---
import { ClientRouter } from 'astro:transitions';
<html lang="en">
<head>
<ClientRouter />
<!-- rest of head -->
</head>
<body>
<slot />
</body>
</html>Pages now cross-fade using the native View Transitions API when available, with a JS polyfill fallback. Name individual elements with transition:name="hero" to animate a specific element across routes.
Image Optimization with Sharp
Astro's <Image /> and <Picture /> components call Sharp to generate responsive variants. On most Ubuntu 24.04 systems Sharp installs prebuilt binaries with no extra steps. On minimal containers you may need libvips:
sudo apt install -y libvips-dev
cd ~/app
npm rebuild sharpUsage:
--- import { Image, Picture } from 'astro:assets'; import hero from '../assets/hero.jpg'; <Image src={hero} alt="Hero" widths={[400, 800, 1200]} sizes="100vw" />
<Picture src={hero} formats={['avif', 'webp']} widths={[400, 800, 1200]} sizes="(max-width: 768px) 100vw, 1200px" alt="Hero" />
For static builds Astro emits all derivatives into dist/_astro/ and serves them with immutable long-cache headers via the Nginx rule you already configured. For SSR routes Sharp transforms run on demand in the Node process; PM2 cluster mode keeps that parallel across cores.
Environment Variables and Secrets
Astro loads .env files during astro dev and astro build following Vite's conventions. Create /home/astro/app/.env.production:
PUBLIC_SITE_URL=https://example.com
DATABASE_URL=postgres://astro:[email protected]:5432/astro
API_TOKEN=redactedVariables prefixed with PUBLIC_ are embedded into the client bundle; everything else is server-only. Access them in server code via import.meta.env.DATABASE_URL (or process.env.DATABASE_URL in Node adapter code).
For runtime-only SSR secrets that should not bake into the build, pass them via PM2's env block or a systemd drop-in. Keep .env.production out of git with a .gitignore entry, and restrict permissions:
chmod 600 /home/astro/app/.env.productionDeploy Workflow and CI/CD
A minimal zero-downtime deploy script on the server:
cat > /home/astro/deploy.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /home/astro/app
git pull --ff-only
npm ci --omit=dev=false
npm run build
pm2 reload astro-ssr --update-env
EOF
chmod +x /home/astro/deploy.shRun ~/deploy.sh on every release. pm2 reload restarts workers one at a time so at least one is always serving traffic -- true zero-downtime deploys.
For CI/CD, a GitHub Actions job that SSH-deploys on push to main:
name: deploy
on: { push: { branches: [main] } }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: appleboy/[email protected]
with:
host: ${{ secrets.VPS_HOST }}
username: astro
key: ${{ secrets.VPS_SSH_KEY }}
script: /home/astro/deploy.shTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
Error: Cannot find module '@astrojs/node' | Adapter not installed | Run npx astro add node and rebuild. |
| 502 Bad Gateway on dynamic routes | PM2 process not running | pm2 status. If offline: pm2 start ecosystem.config.cjs. Check logs: pm2 logs astro-ssr. |
| Static pages load, SSR pages 404 | Nginx falls through to filesystem because port 4321 is not reachable | Check curl http://127.0.0.1:4321/ from the server. Ensure PM2 binds to 127.0.0.1:4321 and Nginx upstream matches. |
Sharp fails with Something went wrong installing "sharp" | Missing libvips or unsupported arch | sudo apt install -y libvips-dev && cd ~/app && npm rebuild sharp. |
| Images not optimized, served at original size | astro:assets not imported or non-local image source | Use import img from '../assets/foo.jpg' (static import), not string URLs, for Astro's Image component to process. |
EADDRINUSE 0.0.0.0:4321 | Another process already using the port | sudo lsof -i :4321. Kill or change the PM2 PORT env. |
| PM2 processes restart in a loop | Unhandled promise rejection or missing env var | pm2 logs astro-ssr --lines 200. Fix the error, pm2 reload. |
Certbot fails with Could not bind to IPv4 | Port 80 already bound | sudo systemctl stop nginx before running Certbot in standalone mode; for --nginx plugin make sure Nginx is running with a valid config. |
| View transitions do not animate | ClientRouter not in base layout, or browser lacks View Transition API | Add <ClientRouter /> to <head> of the shared layout. The fallback polyfill handles older browsers automatically. |
import.meta.env.MY_VAR is undefined in SSR | Variable missing at build time | Ensure .env.production exists before npm run build, or set it in the PM2 env block for runtime-only values. |
pm2 logs astro-ssrTail Nginx logs:
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.logFAQ
Should I use static, SSR, or hybrid?
Start with output: 'static' and opt specific routes into SSR via export const prerender = false. A blog, marketing site, or docs site rarely needs full SSR, and keeping the default static means cacheable HTML on disk, near-zero CPU per request, and no Node dependency for most of your traffic. Reach for output: 'server' only when SSR is the majority case.
How does Astro's performance on a VPS compare to Vercel or Netlify?
For static pages, an Nginx-served dist/ directory is as fast as any CDN edge node for visitors in the same region, and faster than a cold-started serverless function for visitors anywhere. Where managed platforms win is global edge caching out of the box; you can replicate that by putting Cloudflare (free tier is fine) in front of your VPS. For SSR, a PM2 cluster on a 2 vCPU VPS comfortably handles hundreds of requests per second with no cold starts, typically outperforming per-region serverless deployments on p99 latency.
Can I run Astro next to other Node apps on the same VPS?
Yes. PM2 manages any number of apps on one machine. Give each a different port (Astro on 4321, another app on 4322, and so on) and point separate Nginx server blocks at the right upstream. On a 4 GB CloudCore Starter you can comfortably run Astro plus a small Strapi or Directus CMS alongside Postgres.
How do I handle background jobs and cron?
For scheduled tasks use regular Linux cron or systemd timers. For queue-based background work, run a BullMQ or Bree worker as a second PM2 process (instances: 1, exec_mode: 'fork') alongside astro-ssr. Astro itself is strictly a request/response framework and does not ship a job system.
Is SvelteKit a better fit than Astro for full apps?
Astro excels at content-heavy sites where most pages are static and a few are dynamic. SvelteKit is a better fit when the app is application-first -- interactive dashboards, forms-heavy tools, multi-page flows with lots of shared state. Both deploy to a Node VPS the same way; see our SvelteKit on Ubuntu guide for that path.
How do I monitor uptime and errors?
PM2 Plus (pm2.io) gives you a dashboard with logs, metrics, and exception tracking. For infrastructure monitoring, run Uptime Kuma, Netdata, or a Prometheus + Grafana stack on the same VPS or on a second "monitoring" VPS. For error tracking, Sentry has a first-class Astro integration (@sentry/astro) that captures both client and server errors.
Next Steps
Your Astro site is live, fast, and fully under your control. Recommended next steps:
- Put Cloudflare in front of the VPS -- Free tier caches static HTML and assets globally, gives you a second layer of DDoS protection and TLS at the edge, and hides your origin IP.
- Add Plausible or Umami analytics -- Privacy-friendly, self-hostable alternatives to Google Analytics that match Astro's lightweight philosophy.
- Integrate a headless CMS -- Pair Astro with Directus, Strapi, or a Git-based CMS like Decap so non-developers can edit content.
- Compare frameworks -- If parts of your site are actually app-shaped, read our SvelteKit deployment guide and consider splitting marketing (Astro) from app (SvelteKit) behind the same domain.
- Read the official docs -- The Astro team maintains excellent reference documentation at docs.astro.build. Bookmark the Deployment, Content Collections, and Server Islands sections.
Host Your Astro Site on CloudCore Starter>
Everything in this guide runs comfortably on a CloudCore Starter VPS: 2 vCPU, 4 GB RAM, 50 GB NVMe, unmetered bandwidth, full root, deployed in under 60 seconds. Flat monthly price, no build minutes, no function invocations, no egress surprises.>
Launch CloudCore Starter