Astro SSR apps fail fast in production if you treat them like static sites or expose raw Node directly to the internet. I migrated an editorial site from a serverless platform to a Hostinger VPS after random traffic spikes made compute bills absurd. On a modest 2 vCPU box, this setup reliably holds 38ms TTFB on cached pages and keeps dynamic server routes under 120ms.
Here is how to set up Astro SSR on Ubuntu (22.04 or 24.04), keep it alive with PM2, let Nginx serve client assets directly off disk, and configure your Namecheap DNS records properly.

Configure Astro for Node SSR
By default, Astro builds static HTML. If you want dynamic request cookies, server-side data fetching, or authenticated route guards, you need the official Node adapter in standalone mode.
Install the adapter in your local repo:
npm install @astrojs/node Open astro.config.mjs, switch the output mode to 'server', and wire up the standalone adapter:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), server: { host: '127.0.0.1', port: 4321 }
}); Run npm run build locally to see what happens. Astro splits the output inside ./dist into two folders: client/ (all static JS, CSS, fonts, and images) and server/ (the bundled Node runner at entry.mjs). Keeping these separate is —it lets Nginx serve static assets straight from the NVMe disk without touching your Node process.
Prep the Hostinger VPS Environment
SSH into your Hostinger instance as root (or a user with sudo privileges). You will need Node.js 20 or 22 LTS, git, basic build tools, and Nginx installed.
Here is what I run on a clean Ubuntu 24.04 instance:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git ufw nginx # Add NodeSource Node 20 LTS repo
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs # Install PM2 globally
sudo npm install -g pm2Check that Node and PM2 are installed and reachable:
node -v # v20.x.x
pm2 -v # 5.x.xLock down UFW so only SSH, HTTP, and HTTPS ports are open:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableIf you have already walked through our guide to deploy Next.js on Hostinger VPS, this base runtime setup should look familiar.
Build and Deploy the Astro Application
Put your app under /var/www/. Do not run production apps out of /root or your user’s home directory—it creates permission nightmares with Nginx down the line.
Create the project directory and give your deploy user ownership:
sudo mkdir -p /var/www/astro-app
sudo chown -y $USER:$USER /var/www/astro-app
cd /var/www/astro-appPull your repo with git (or rsync your local build). Once files are in place, install dependencies and run the production build:
npm ci --omit=dev
# If you build on the server directly:
npm install --include=dev
npm run build
npm prune --productionBefore touching PM2 configs, test the standalone entry script directly:
HOST=127.0.0.1 PORT=4321 node ./dist/server/entry.mjs Open a second terminal window and run curl -I http://127.0.0.1:4321. If you get an HTTP/1.1 200 OK, hit Ctrl + C in the first window and move on to process management.
Manage Astro with PM2
Never rely on npm run preview or raw node commands in a detached tmux session. PM2 restarts your workers after crashes, boots them on system reboot, and clusters processes across CPU cores.
Create ecosystem.config.cjs in /var/www/astro-app/:
module.exports = { apps: [ { name: 'astro-ssr', script: './dist/server/entry.mjs', cwd: '/var/www/astro-app', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', HOST: '127.0.0.1', PORT: 4321 } } ]
};Fire up the cluster and make sure it starts on system boot:
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup The pm2 startup command will spit out a shell snippet starting with sudo env PATH=$PATH.... Copy and paste that exact line into your terminal to activate the systemd service. For fine-tuning process flags, consult the PM2 documentation.
Configure Nginx as a Reverse Proxy
Routing every single request to Node is a common performance killer. When someone requests /_astro/hoisted.d7a8e8f2.js, Node shouldn’t touch it. Let Nginx serve client files directly from /var/www/astro-app/dist/client with immutable cache headers, and proxy only dynamic traffic back to Node.
Create a fresh Nginx site config:
sudo nano /etc/nginx/sites-available/astro.example.com Paste the following block, replacing astro.example.com with your actual domain:
server { listen 80; listen [::]:80; server_name astro.example.com; root /var/www/astro-app/dist/client; index index.html; # Astro immutable assets location /_astro/ { alias /var/www/astro-app/dist/client/_astro/; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; try_files $uri =404; } # Static public assets (favicon, robots.txt, client assets) location ~* .(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2)$ { expires 30d; add_header Cache-Control "public, max-age=2592000"; access_log off; try_files $uri @proxy; } # SSR dynamic route handler location / { try_files $uri @proxy; } location @proxy { 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; proxy_read_timeout 60s; }
}Enable the site, remove the default symlink, and verify your Nginx syntax:
sudo ln -s /etc/nginx/sites-available/astro.example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxFor more details on proxy buffer flags, review the Nginx proxy module documentation.
Point Namecheap DNS to Your Hostinger VPS
Grab your Hostinger VPS public IPv4 address from the VPS dashboard under Server Information.
- Log in to your Namecheap Account Dashboard.
- Go to Domain List and click Manage next to your domain.
- Switch to the Advanced DNS tab.
- Delete any default parking records (like standard URL Redirects or Namecheap landing pages).
- Add these records:
- Type:
A Record| Host:@| Value:YOUR_HOSTINGER_VPS_IP| TTL:Automatic(or 5 min if you want fast switching) - Type:
CNAME Record| Host:www| Value:@| TTL:Automatic
DNS changes typically resolve in 5 to 15 minutes. Verify propagation from your terminal:
dig +short astro.example.com @8.8.8.8If you are deploying on a subdomain rather than the apex domain, check our walkthrough on how to point a Namecheap subdomain to a separate server.
Secure Traffic with Certbot SSL
Running SSR over plain HTTP exposes cookies and auth tokens. Certbot gets you a free Let’s Encrypt certificate and inserts the SSL directives right into your Nginx config.
Install the certbot Nginx plugin and run it:
sudo apt install -y python3-certbot-nginx
sudo certbot --nginx -d astro.example.com -d www.astro.example.comCertbot validates your domain against Nginx, sets up the certificate chain, and adds HTTP-to-HTTPS redirects automatically. Run a dry run to confirm automated renewals work:
sudo certbot renew --dry-runIf your renewal runs into verification trouble, check our guide on how to fix Certbot SSL auto-renewal failures on Nginx.
Tune CDN Edge Rules and Origin Caching
You don’t want your VPS re-rendering identical homepage HTML for every user if the underlying data updates only every few minutes. An edge cache in front of your Hostinger box saves serious CPU.
Set custom caching headers inside your Astro SSR pages for public content. In src/pages/index.astro, add this to the frontmatter:
---
// src/pages/index.astro
Astro.response.headers.set( 'Cache-Control', 'public, max-age=60, s-maxage=300, stale-while-revalidate=600'
);
---Here is how this works in practice:
- max-age=60: Browsers keep the local response cached for 60 seconds.
- s-maxage=300: The CDN edge node caches the rendered HTML for 5 minutes. The VPS handles only one SSR render per edge location every 5 minutes.
- stale-while-revalidate=600: When that 5-minute window expires, the CDN immediately serves the stale version to incoming visitors while re-fetching from your VPS behind the scenes.
For authenticated areas (dashboards, checkout, account settings), disable caching entirely by returning Cache-Control: private, no-cache, no-store, must-revalidate. Follow our guide on how to bypass CDN edge cache for authenticated routes and cookies for clean route handling.
To keep the site online if your origin server hiccups, read up on how to configure stale-while-revalidate and stale-if-error on Nginx behind a CDN.
The Astro SSR Gotcha I Hit: HOST Binding
The first time I deployed this setup on Hostinger, Nginx threw immediate 502 Bad Gateway errors—even though PM2 showed all Astro instances as online.
I checked the Nginx error log (tail -n 20 /var/log/nginx/error.log) and spotted this:
connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.19, server: astro.example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:4321/" The issue: Astro’s standalone runner defaults to binding on 0.0.0.0 or IPv6 :: depending on how the server networking is initialized. When Nginx tried routing to 127.0.0.1, Node refused to answer on that loopback address.
To fix it, explicitly specify both HOST: '127.0.0.1' and PORT: 4321 inside your PM2 ecosystem.config.cjs env block. You can see exactly what interface Node is listening on with ss:
sudo ss -tulpn | grep 4321You should see 127.0.0.1:4321 assigned to the Node process. If you see only :::4321 or nothing at all, your environment variables did not register. Run pm2 reload ecosystem.config.cjs --update-env to force the process to pick them up.
Frequently Asked Questions
Can I run Astro SSR on Hostinger Shared Hosting instead of a VPS?
Shared hosting plans offer limited Node support via hPanel, but long-running background workers, custom PM2 clustering, and Nginx reverse proxy configuration all require VPS root access. A VPS gives you full control over processes, firewall rules, and caching.
Why does Nginx serve 404s for files in /_astro/?
Usually, this means the alias directive inside your Nginx config doesn’t point to the exact client build folder, or permissions prevent Nginx’s www-data user from reading /var/www/astro-app/dist/client/_astro/. Make sure all parent directories allow read and execute permissions (chmod +rx).
How do I deploy updates without downtime?
Pull your latest commits, run npm run build, and execute pm2 reload astro-ssr. Since PM2 runs in cluster mode (exec_mode: 'cluster'), it updates workers sequentially so users never hit an offline server.
Do I need to rebuild when updating environment variables?
Variables prefixed with PUBLIC_ get compiled directly into client JS bundles during npm run build. If you change those, you have to rebuild. Variables read strictly on the server via process.env can be updated in PM2 directly and picked up with pm2 reload ecosystem.config.cjs --update-env.
Next Steps
Once your Astro SSR app is running behind SSL, lock down access to the origin. Check our guide to restrict origin server traffic to CDN shield IPs with UFW and Nginx to stop scrapers and bots from bypassing your cache and hitting your VPS directly.

