Most frameworks dump a megabyte of JavaScript on your users and call it a web app. Astro ships zero JavaScript by default. It builds in your components, renders them at compile time, and sends the browser plain HTML unless you explicitly ask for interactivity. That's the deal, and it's a genuinely different contract from what React or Next gives you.
This guide covers both deployment modes: static (Nginx serves pre-built HTML files) and SSR (Node.js adapter, PM2 process manager, Nginx reverse proxy). Pick the one that fits your project.
Astro is a content-focused web framework that treats JavaScript as optional. Pages are .astro files: a mix of a frontmatter script block and HTML-like templating. You can drop in React, Vue, Svelte, Solid, or Preact components alongside native .astro components, and Astro handles the build.
The output depends on the mode you configure:
Static mode (output: 'static') builds every page into flat .html files at compile time. Nginx serves them directly — no Node process, no runtime overhead. Ideal for blogs, docs, marketing sites, and anything where content doesn't change per request.
Server mode (output: 'server') runs a Node.js server that renders pages on demand. You need this for dynamic routes, form handling, auth sessions, per-user content, or API endpoints that depend on request context.
Worth knowing before you start: Astro 6.x requires Node.js 22.12.0 or higher. If your server is running a lower version, update Node before anything else — the build will fail without it.
What Astro won't do well: it's not a replacement for a full backend. API routes work, but if you're building a heavy REST API or a real-time WebSocket server, you'll be fighting the framework. For content-driven sites and lightweight SSR, it's excellent.
|
Criteria |
Static |
SSR |
|
Runtime |
None (Nginx only) |
Node.js + PM2 |
|
Per-request logic |
No |
Yes |
|
Auth / sessions |
No |
Yes |
|
Memory on VPS |
~5 MB (Nginx) |
~80–150 MB (Node) |
|
Rebuild needed for content updates |
Yes |
No |
|
Recommended is*hosting plan |
Lite (1 vCPU / 1 GB) |
Start (2 vCPU / 2 GB) |
Self-hosting gives you something managed platforms don't: control over where your server runs. That matters if your users are in a specific region, if you're subject to data residency rules, or if you just don't want your site's traffic routed through a US-based CDN by default. is*hosting has VPS locations across 40+ countries, so you can put the server close to your audience or in the jurisdiction that fits your situation. Platforms like Vercel are convenient, but they own your deployment pipeline. With a VPS, you do.
Minimum requirements:
For a static site: The Lite plan (1 vCPU / 1 GB RAM, from $5.94/mo) is enough. Nginx serving pre-built HTML barely touches memory.
For SSR: Use the Start plan (2 vCPU / 2 GB RAM / 30 GB NVMe, from $10.19/mo on annual billing). The Node.js process sits at 80–150 MB at idle. On a 1 GB server, that leaves little room for npm builds, which can spike to 300–400 MB during compilation.
40+ locations. KVM, dedicated resources. Free weekly baclups. Deploy in 5-15 minutes.
Note: Node.js is not pre-installed on is*hosting VPS plans, but root SSH access is available on all of them. Setup takes about three minutes and is covered in Step 2.
On Mac or Linux:
ssh root@YOUR_VPS_IP
On Windows, use PowerShell or Tabby. Type yes at the fingerprint prompt, paste your password, and when you see root@hostname:~#, you're in.
Update the package list first:
apt update && apt upgrade -y
Astro 6 requires Node.js 22.12.0 minimum. The version in Ubuntu's default repos is older, so use NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
Confirm both the version and npm:
node -v # should show v22.x.x
npm -v
If the node -v output is below 22.12, the NodeSource script didn't run correctly. Rerun it as root before continuing.
apt install -y nginx
systemctl enable nginx
systemctl start nginx
Check that it's up:
curl http://localhost
You should see the default Nginx welcome page HTML. That's your signal to move on.
apt install -y ufw
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Astro's Node server binds to 127.0.0.1:4321 by default — loopback only, not externally reachable. Only Nginx (ports 80 and 443) needs to be public. Confirm the rules are correct:
ufw status
Expected output:
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
If your project is in a Git repo:
git clone https://github.com/your-username/your-astro-project.git
cd your-astro-project
npm install
If you're copying local files over, use scp or rsync:
rsync -avz ./your-astro-project root@YOUR_VPS_IP:/var/www/your-astro-project
Then on the server:
cd /var/www/your-astro-project
npm install
Open astro.config.mjs in nano or vim.
For static output (the default; no changes needed if you haven't touched this file):
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
});
For SSR with the Node adapter, first add the adapter:
npx astro add node
That command installs @astrojs/node and automatically patches your config. The result:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
});
standalone mode is what you want for VPS deployment. It builds a self-contained server at dist/server/entry.mjs that starts up with a single node command — no Express wrapper or additional glue code needed. The adapter reads PORT and HOST from environment variables at runtime, so you can control the bind address without touching the build.
npm run build
The first build takes 30–90 seconds, depending on project size. Output:
If the build errors with Cannot find module, you likely have a Node version mismatch. Run node -v and compare it against the engines field in package.json.
Create a new Nginx config:
nano /etc/nginx/sites-available/astro
Paste this:
server {
listen 80;
server_name your.domain.com;
root /var/www/your-astro-project/dist;
index index.html;
location / {
try_files $uri $uri/ $uri.html =404;
}
# Serve pre-compressed files if available
gzip_static on;
}
Enable it and restart:
ln -s /etc/nginx/sites-available/astro /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
nginx -t checks the syntax before you reload. If it says test failed, read the error line — it usually points directly to the problem.
Visit your VPS IP in a browser, and you should see your Astro site.
PM2 is a Node.js process manager. It keeps your app running after crashes and restarts it on server reboot.
npm install -g pm2
cd /var/www/your-astro-project
HOST=127.0.0.1 PORT=4321 pm2 start dist/server/entry.mjs --name "astro-app"
HOST=127.0.0.1 binds the Node server to loopback only — it won't be reachable directly from the internet, only through Nginx. PORT=4321 is Astro's default; change it if that port is taken.
Check that it started:
pm2 status
You should see astro-app with the status online. Check the live log output:
pm2 logs astro-app --lines 20
Look for Server listening on http://127.0.0.1:4321. That's your confirmation.
pm2 startup
PM2 will print a sudo env PATH=... command. Copy it exactly and run it. Then save the current process list:
pm2 save
After a server reboot, PM2 will restore and restart astro-app automatically.
Create the config:
nano /etc/nginx/sites-available/astro
server {
listen 80;
server_name your.domain.com;
location / {
proxy_pass http://127.0.0.1:4321;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
Enable it:
ln -s /etc/nginx/sites-available/astro /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
This applies to both static and SSR setups. You need a domain pointing at your VPS IP before running this.
apt install -y certbot python3-certbot-nginx
certbot --nginx -d your.domain.com
Certbot will modify your Nginx config and set up automatic renewal via a systemd timer. Test that the renewal works:
certbot renew --dry-run
Pull new code, rebuild, and reload.
Static:
cd /var/www/your-astro-project
git pull
npm install
npm run build
systemctl reload nginx
SSR:
cd /var/www/your-astro-project
git pull
npm install
npm run build
pm2 restart astro-app
Before any major Astro version update (npm update astro), back up the build:
cp -r dist dist.bak
is*hosting includes free weekly VPS backups on all plans, but those are full-server snapshots. A targeted copy of dist/ is faster to restore if a bad deploy breaks something.
The Node adapter handles both static assets and SSR routes. In standalone mode, dist/server/entry.mjs serves files from dist/client/ automatically. You don't need a separate Nginx block for static assets when running SSR — the Node server handles it. The Nginx config above just proxies everything to Node.
Environment variables at build time vs. runtime work differently. Secrets in .env at build time get baked into the output. For runtime secrets (database passwords, API keys, etc.), provide them as environment variables when starting the server:
DATABASE_URL=postgres://... PORT=4321 pm2 start dist/server/entry.mjs --name "astro-app"
Or define them in a PM2 ecosystem file to keep things clean:
nano ecosystem.config.cjs
module.exports = {
apps: [{
name: 'astro-app',
script: './dist/server/entry.mjs',
env: {
HOST: '127.0.0.1',
PORT: 4321,
DATABASE_URL: 'postgres://...',
},
}],
};
Then: pm2 start ecosystem.config.cjs
Hybrid output lets you mix static and SSR per page. Set output: 'server' globally and add export const prerender = true to individual pages that you want pre-built at deploy time. Those pages get served as static files; the rest render on demand. It's useful when most of a site is static, but a few routes need per-request logic.
You can skip the manual setup entirely with Coolify. If you'd rather not manage Nginx, PM2, and SSL by hand, is*hosting offers a pre-installed Coolify image directly from the VPS configurator. Coolify is an open-source PaaS that handles Git-based deploys, automatic SSL, and one-click app management from a web dashboard. Select the Coolify image at checkout, and you'll get a ready-to-use deployment platform on first boot. Connect your repo, point it at your Astro project, and Coolify handles the rest — including rebuilds on every push. The is*hosting guide to self-hosting with Coolify covers the full setup if you want to go that route.
Build fails with "Cannot find module" or Node version error. Run node -v. If it's below 22.12.0, reinstall Node via the NodeSource script in Step 2.
PM2 shows errored status. Run pm2 logs astro-app --lines 50. The most common cause is a missing environment variable or a port conflict. Check PORT is free with ss -tlnp | grep 4321.
Nginx returns 502 Bad Gateway. The Node server isn't running or isn't listening on the expected port. Run pm2 status and check logs. If the app is offline, pm2 restart astro-app.
Static site returns 404 on direct URL access. The try_files $uri $uri/ $uri.html =404; line in the Nginx config handles this. If it's missing from your config, Nginx won't know to look for about.html when someone hits /about.
Astro deploys in under 20 minutes on any Linux VPS. A static build needs only Nginx and uses negligible resources. SSR adds Node and PM2 but stays lightweight; a well-configured Start plan handles it without breaking a sweat.
If you need a server, is*hosting VPS plans start at $5.94/mo for static sites and $10.19/mo for SSR. If you want to add automation workflows alongside your Astro site, is*hosting also has a ready-to-deploy n8n VPS that pairs well here.