Why self-host Astro on a VPS
Astro has a peculiarity: it can produce either a 100% static site (output: 'static'), or an application with on-demand server rendering (output: 'server' via @astrojs/node). This duality makes the VPS very relevant. In static mode, you serve HTML files directly from Nginx, without any Node process: maximum performance and minimal footprint. In SSR, you launch a Node server that renders pages dynamically, useful for personalized content or integrated API endpoints. On a VPS, you choose your strategy without being locked into the model imposed by a front-end deployment platform. You also control the HTTP cache, the security headers and Brotli compression, where front-end hosts handle that as a black box.
Concrete benefits
- Freedom of output mode: pure static or SSR Node, or even hybrid per route
- In static mode, Nginx serves the files without any Node process: near-zero RAM, maximum throughput
- Full control of the HTTP cache, security headers (CSP, HSTS) and Brotli compression
- No bandwidth limit or request quota imposed by a front-end platform
- Astro API endpoints hosted on the same domain, without a third-party serverless function
- Reproducible deployment via Docker: the same build locally and in production
Hardware and software prerequisites
A static Astro site fits on the smallest VPS imaginable: 1 vCPU and 1 GB of RAM is more than enough, since Nginx only serves files. In SSR mode, plan for 2 GB for the Node process and rendering peaks. Install Node.js 20 LTS to build (or build in CI), Docker if you containerize, and Nginx as a front end. Point mydomain.com and www.mydomain.com at the VPS IP. Count 5 GB of disk; the dist/ folder of a static site generally weighs a few MB.
Step-by-step deployment
Choose the output mode
In astro.config.mjs, set output: 'static' for a content site, or output: 'server' with adapter: node({ mode: 'standalone' }) if you need SSR. This choice conditions the whole rest of the deployment.
Build the application
Run npm run build. In static mode, you get a dist/ folder containing the HTML, CSS and JS ready to serve. In SSR, you additionally get a dist/server/entry.mjs to run with Node.
Serve the static files with Nginx (static mode)
Point your vhost's root directive at /var/www/astro/dist and enable try_files $uri $uri/ /404.html;. Add gzip on; and long Cache-Control headers on versioned assets. No container is needed.
Containerize the Node server (SSR mode)
For SSR, create a Dockerfile based on node:20-alpine that copies dist/ and runs node ./dist/server/entry.mjs. Expose port 4321 and start with docker compose up -d. Nginx will then do a proxy_pass to this port.
Configure the reverse proxy and compression
Whether you are in static mode or in SSR behind Node, declare the Nginx vhost with www redirected to the apex domain. Enable Brotli if the module is available for assets even lighter than gzip.
Enable SSL
Run certbot --nginx -d mydomain.com -d www.mydomain.com. Force the redirect to HTTPS and add the Strict-Transport-Security header. Validate the deployment with a Lighthouse test from your browser.
Even for an SSR site, leverage Astro's hybrid rendering: mark static pages with export const prerender = true so they are generated at build time and served directly by Nginx, reserving the Node process only for genuinely dynamic routes. This drastically reduces the server load. And place an Nginx proxy_cache in front of low-change-frequency SSR pages to absorb traffic spikes.