is*hosting Blog & News - Next Generation Hosting Provider

Astro on a VPS: Static and SSR Deployment Tutorial

Written by is*hosting team | Jun 25, 2026 4:00:00 AM

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.

What Astro Does

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.

Static vs. SSR: When Self-Hosting Makes Sense

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.

What You Need Before Setup

Minimum requirements:

  • A VPS running Ubuntu 22 or 24
  • SSH access with root or sudo
  • Node.js 22.12.0 or higher (installed below)
  • Nginx
  • A domain name (optional, but needed for HTTPS)
  • For SSR only: PM2

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.

VPS

40+ locations. KVM, dedicated resources. Free weekly baclups. Deploy in 5-15 minutes.

Watch plans

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.

How to Deploy Astro on a VPS

Step 1: Connect and Update

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

Step 2: Install Node.js 22

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.

Step 3: Install Nginx

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.

Step 4: Configure the Firewall

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

Step 5: Get Your Astro Project on the Server

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

Step 6: Configure Astro for Your Deployment Mode

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.

Step 7: Build

npm run build

The first build takes 30–90 seconds, depending on project size. Output:

  • Static: dist/ contains .html, .css, and .js files. Nginx serves these directly.
  • SSR: dist/client/ holds static assets; dist/server/entry.mjs is the server entrypoint.

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.

Serving a Static Astro Site with Nginx

Step 8a: Configure Nginx for Static

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.

Serving an SSR Astro Site with PM2 + Nginx

Step 8b: Install PM2

PM2 is a Node.js process manager. It keeps your app running after crashes and restarts it on server reboot.

npm install -g pm2

Step 9: Start the Astro Server with 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.

Step 10: Configure PM2 to Start on Boot

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.

Step 11: Configure Nginx as a Reverse Proxy

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

Adding HTTPS with Certbot

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

Updating Astro

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.

Three Things Worth Knowing Before You Go Live

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.

Troubleshooting

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.

What You've Got Running

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.