How to Deploy Astro on a VPS: Static and SSR Modes with Node Adapter and Nginx

How to Deploy Astro on a VPS: Static and SSR Modes with Node Adapter and Nginx

Astro is a content-focused web framework that builds fast websites through its “islands” architecture — components are rendered to static HTML at build time, with JavaScript hydration only for interactive elements. Deploying Astro on a VPS is flexible: purely static sites (no JavaScript on the server) need only Nginx; sites using server-side rendering with Astro’s Node adapter need Node.js and PM2. This guide covers both modes.

Astro Output Modes

  • output: 'static' (default): Builds to a directory of HTML/CSS/JS files. Nginx serves them directly — zero Node.js required at runtime. Maximum performance, zero server-side overhead.
  • output: 'server': Full SSR — every page rendered on demand. Requires Node adapter and a running Node.js process.
  • output: 'hybrid': Mixed — most pages pre-rendered to static, specific pages/endpoints SSR. Requires Node adapter.

Option A: Static Astro Build (Nginx-Only Deployment)

Build Locally

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  output: 'static',    // default, can omit
  build: {
    assets: '_astro',
  },
});
npm run build
# Creates dist/ directory with all static files

Deploy Static Files to VPS

rsync -avz --delete ./dist/ deploy@YOUR_VPS_IP:/var/www/myastrosite/

Nginx Configuration for Static Astro

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    root /var/www/myastrosite;
    index index.html;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    # Astro's _astro/ directory contains versioned hashed assets — cache forever
    location /_astro/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    gzip on;
    gzip_types text/html text/css application/javascript application/json image/svg+xml;

    location / {
        try_files $uri $uri/ $uri.html =404;
    }
}
sudo ln -s /etc/nginx/sites-available/myastrosite /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Option B: SSR Astro with Node Adapter

Configure for SSR

npm install @astrojs/node
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',   // Generates a self-contained server in dist/
  }),
});
npm run build
# Creates dist/server/entry.mjs and dist/client/

PM2 Configuration for Astro SSR

// /var/www/myastrosite/ecosystem.config.cjs
module.exports = {
  apps: [{
    name: 'astro-ssr',
    script: '/var/www/myastrosite/dist/server/entry.mjs',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      HOST: '127.0.0.1',
      PORT: 4321,
    },
    max_memory_restart: '512M',
  }],
};
pm2 start /var/www/myastrosite/ecosystem.config.cjs
pm2 save && pm2 startup

Nginx Reverse Proxy for Astro SSR

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    # Serve static assets directly (faster than proxying)
    location /_astro/ {
        alias /var/www/myastrosite/dist/client/_astro/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # SSR pages proxy to Node.js
    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-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

GitHub Actions Deployment Pipeline

# .github/workflows/deploy.yml
name: Deploy Astro

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci && npm run build

      - name: Sync to VPS
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no" \
            ./dist/ deploy@${{ secrets.VPS_HOST }}:/var/www/myastrosite/

      # For SSR mode only — reload PM2
      - name: Reload PM2 (SSR)
        if: false
        run: |
          ssh -o StrictHostKeyChecking=no deploy@${{ secrets.VPS_HOST }} \
            "pm2 reload astro-ssr --update-env"

Hybrid Mode: Best of Both Worlds

Astro’s hybrid mode pre-renders everything to static HTML except routes explicitly marked as SSR — ideal for blogs with a few dynamic pages (search, user accounts, API routes):

// astro.config.mjs
export default defineConfig({
  output: 'hybrid',
  adapter: node({ mode: 'standalone' }),
});
// src/pages/search.astro — This page is SSR only
---
export const prerender = false;
const query = Astro.url.searchParams.get('q');
const results = await searchDatabase(query);
---
<SearchResults results={results} />

Performance: Astro on VPS vs Vercel/Netlify

  • Static mode: Nginx-served Astro is as fast or faster than Vercel/Netlify CDN for single-region audiences — TTFB under 50ms from NVMe-backed Nginx. Add Cloudflare for global CDN distribution.
  • SSR mode: No cold starts (Node.js process always running), no function timeout limits, full WebSocket support.
  • Cost: After Vercel/Netlify free tier limits, a $10/month VPS serves comparable traffic at lower cost.

Getting Started

Static Astro needs minimal VPS resources — any Ubuntu VPS at VPS.DO handles static Astro sites with Nginx effortlessly. For SSR mode, 1 GB RAM is the practical minimum; 2 GB is comfortable with room for growth. NVMe storage keeps Nginx’s static file serving fast and build artifact transfers quick.

Conclusion

Astro’s dual output modes make VPS deployment straightforward for any use case: static output requires only Nginx and delivers exceptional performance with zero server-side overhead; SSR/hybrid mode with the Node adapter and PM2 enables dynamic pages without cold starts or execution time limits. Both are self-hosted alternatives to Netlify and Vercel that reduce costs at meaningful traffic volumes while providing complete infrastructure control.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!