DeployWise
HomeGuidesDeploy Astro to VPS
FrameworkVPS DeploymentAdvanced

Deploy Astro to VPS: Complete Guide 2026

Master the complete process of deploying Astro applications to a VPS with server-side rendering, process management, and production-grade security. This guide covers everything from choosing between SSR and static builds to automating deployments with DeployWise.

12 min read
Updated February 28, 2026

Astro SSR vs Static: Choosing Your Deployment Model

Before deploying Astro to a VPS, you need to decide between two deployment models:

Static (Default)

  • +Pre-rendered HTML, maximum performance
  • +No server needed, works on any static host
  • +Excellent SEO with pre-rendered pages
  • -No dynamic content or user sessions
  • -Rebuild required for content updates

SSR (Server-Side Rendering)

  • +Dynamic content and user sessions
  • +Real-time data and database queries
  • +No rebuild needed for content changes
  • -Requires Node.js server (VPS needed)
  • -Higher server resource requirements

For this guide, we'll focus on SSR deployment since it requires a VPS. Static sites can be deployed to Vercel, Netlify, or any CDN with a single command.

Setting Up @astrojs/node Adapter

The @astrojs/node adapter enables SSR for Astro applications on Node.js environments. Install and configure it for your VPS deployment.

Step 1: Install the Adapter

bash
npm install @astrojs/node

Step 2: Update astro.config.mjs

javascript
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
});

The mode: 'standalone' option creates a fully self-contained server that can run independently without needing Astro's dev server.

Key Adapter Options

  • mode: 'standalone' or'middleware'. Use standalone for VPS.
  • start: Custom start command (optional)
  • host: Listening host, default localhost
  • port: Server port, default 3000

Build Configuration and Output Settings

Understanding the difference between output: 'static' andoutput: 'server' is crucial for VPS deployment.

Static vs Server Output

output: 'static'

  • • Generates dist/ with HTML files
  • • No Node.js runtime needed
  • • Best for static sites and CDNs

output: 'server'

  • • Generates dist/server/ for Node.js
  • • Includes client assets in dist/client/
  • • Requires Node.js runtime on VPS
  • • Enables dynamic rendering and SSR

Complete astro.config.mjs for VPS

javascript
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
  vite: {
    ssr: {
      external: ['sharp'],
    },
  },
});

Building for Production

bash
npm run build

This generates dist/ with:

  • dist/server/ - Node.js entry point
  • dist/client/ - Static assets (CSS, JS, images)
  • package.json - Dependencies in dist root

PM2 Process Management Configuration

PM2 keeps your Astro application running, handles automatic restarts, and manages multiple processes. It's production-ready process management for Node.js applications.

Step 1: Install PM2 Globally

bash
npm install -g pm2

Step 2: Create PM2 Configuration (ecosystem.config.js)

javascript
module.exports = {
  apps: [
    {
      name: 'astro-app',
      script: './dist/server/entry.mjs',
      exec_mode: 'cluster',
      instances: 'max',
      autorestart: true,
      watch: false,
      env: {
        NODE_ENV: 'production',
        HOST: '127.0.0.1',
        PORT: 3000,
      },
      error_file: './logs/err.log',
      out_file: './logs/out.log',
      log_file: './logs/combined.log',
      time_format: 'YYYY-MM-DD HH:mm:ss Z',
      max_memory_restart: '500M',
      max_restarts: 10,
      min_uptime: '10s',
    },
  ],
};

Step 3: Start the Application

bash
# Start the application
pm2 start ecosystem.config.js

# Save PM2 configuration
pm2 save

# Enable startup on system boot
pm2 startup
pm2 save

Useful PM2 Commands

  • pm2 list - Show all running processes
  • pm2 logs astro-app - View application logs
  • pm2 restart astro-app - Restart the application
  • pm2 stop astro-app - Stop the application
  • pm2 delete astro-app - Remove from PM2
  • pm2 monit - Real-time monitoring dashboard

Nginx Reverse Proxy Setup

Nginx acts as a reverse proxy, forwarding requests from port 80/443 to your Node.js application running on port 3000. This setup provides better performance and security.

Step 1: Install Nginx

bash
# Ubuntu/Debian
sudo apt update
sudo apt install nginx

# Start Nginx
sudo systemctl start nginx
sudo systemctl enable nginx

Step 2: Create Nginx Configuration

Create /etc/nginx/sites-available/astro.conf:

nginx
upstream astro_app {
  server 127.0.0.1:3000;
  keepalive 32;
}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;

  # Redirect HTTP to HTTPS
  return 301 https://$server_name$request_uri;
}

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

  # SSL certificates (configure after Let's Encrypt setup)
  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  # SSL configuration
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;

  # Security headers
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header X-XSS-Protection "1; mode=block" always;

  # Gzip compression
  gzip on;
  gzip_types text/plain text/css text/javascript application/json application/javascript;
  gzip_min_length 1000;

  # Client upload size
  client_max_body_size 50M;

  location / {
    proxy_pass http://astro_app;
    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;
    proxy_read_timeout 60s;
    proxy_connect_timeout 60s;
  }

  # Static asset caching
  location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
  }
}

Step 3: Enable the Configuration

bash
# Create symlink
sudo ln -s /etc/nginx/sites-available/astro.conf /etc/nginx/sites-enabled/

# Test Nginx configuration
sudo nginx -t

# Reload Nginx
sudo systemctl reload nginx

SSL Certificates with Let's Encrypt

Certbot automates the installation and renewal of free SSL certificates from Let's Encrypt, ensuring your application is always served over HTTPS.

Step 1: Install Certbot

bash
# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx

# Verify installation
certbot --version

Step 2: Obtain and Install Certificate

bash
# Certbot will automatically update your Nginx config
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Step 3: Enable Auto-Renewal

bash
# Check renewal configuration
sudo certbot renew --dry-run

# Enable auto-renewal timer
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

# Verify timer is running
sudo systemctl status certbot.timer

Let's Encrypt certificates are valid for 90 days and auto-renew 30 days before expiration.

Common Mistakes and How to Avoid Them

Mistake 1: Forgetting output: 'server' Configuration

The most common error is deploying with output: 'static' (the default) instead of output: 'server'.

javascript
// ❌ WRONG - This won't work with @astrojs/node
export default defineConfig({
  // output: 'static' by default
  adapter: node(),
});

// ✅ CORRECT
export default defineConfig({
  output: 'server',
  adapter: node(),
});

Mistake 2: Looking for dist/ Instead of dist/server/

SSR builds output to dist/server/entry.mjs, not the rootdist/ directory.

bash
# ❌ WRONG - This file doesn't exist with SSR
node dist/entry.mjs

# ✅ CORRECT - SSR entry point
node dist/server/entry.mjs

# Or use PM2 with ecosystem.config.js
pm2 start ecosystem.config.js

Mistake 3: Not Installing Dependencies on VPS

The build directory needs node_modules on the VPS.

bash
# Deploy process:
# 1. Build locally or in CI/CD
npm run build

# 2. Upload dist/ folder to VPS
# 3. Install dependencies on VPS
cd /var/www/astro-app
npm install --production

# 4. Start with PM2
pm2 start ecosystem.config.js

Mistake 4: Incorrect Nginx Upstream Configuration

Make sure Nginx connects to the correct port where your Node.js app is running.

bash
# Check which port your app is using
pm2 logs astro-app | grep "listening on"

# Update Nginx upstream to match
upstream astro_app {
  server 127.0.0.1:3000;  # Change 3000 to your actual port
  keepalive 32;
}

Mistake 5: Missing Environment Variables

Astro applications often need environment variables (database URLs, API keys, etc.).

bash
# Create .env file on VPS
/var/www/astro-app/.env
DATABASE_URL=postgres://...
API_KEY=your-api-key

# Update ecosystem.config.js to include env variables
{
  apps: [{
    name: 'astro-app',
    script: './dist/server/entry.mjs',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      DATABASE_URL: process.env.DATABASE_URL,
      API_KEY: process.env.API_KEY,
    },
  }],
}

Mistake 6: Adapter Mode Confusion

The mode setting changes how the adapter works.

javascript
// mode: 'standalone' (recommended for VPS)
// Creates a self-contained server, run with: node dist/server/entry.mjs
adapter: node({ mode: 'standalone' })

// mode: 'middleware'
// Requires Express.js or other framework integration
adapter: node({ mode: 'middleware' })

// For VPS deployment, always use 'standalone'

Automating with DeployWise

DeployWise automates the entire Astro deployment process, eliminating manual configuration errors and reducing deployment time from hours to minutes.

What DeployWise Handles Automatically

Detects Astro projects automatically
Configures @astrojs/node adapter
Sets output: 'server' option
Generates PM2 configuration
Sets up Nginx reverse proxy
Installs SSL certificates
Handles environment variables
Manages builds and deployments

Quick Start with DeployWise

bash
# 1. Connect your VPS to DeployWise
# 2. Create a new project and select your Git repository
# 3. DeployWise detects your Astro project
# 4. Configure deployment settings (domain, environment variables)
# 5. Deploy with a single click

# That's it! DeployWise handles:
# - Building your Astro project
# - Uploading to VPS
# - Installing dependencies
# - Configuring PM2
# - Setting up Nginx
# - Installing SSL certificates
# - Monitoring the application

DeployWise Benefits for Astro

  • Zero configuration - handles Astro-specific settings automatically
  • One-click deployments with rollback capability
  • Real-time monitoring and logs
  • Automatic SSL renewal and security updates
  • Git integration with automatic deployments on push

Ready to Deploy Your Astro App?

Skip the manual configuration and deploy your Astro application to VPS in minutes with DeployWise.

Get Started with DeployWise

Quick Reference: Deployment Checklist

  • Install @astrojs/node adapter
  • Set output: 'server' in astro.config.mjs
  • Build project with npm run build
  • Create ecosystem.config.js for PM2
  • Configure Nginx reverse proxy
  • Install SSL certificate with Certbot
  • Start application with PM2
  • Monitor logs and performance

Related guides