Deploying Astro
Astro is unusually flexible about where it runs. By default it ships a fully static site — plain HTML, CSS, and only the JavaScript your islands actually need — which any file host on the planet can serve. When you need server logic, you swap in an adapter and the very same project produces a server bundle for Node, a serverless platform, or an edge runtime. Understanding which output mode you’re targeting is the single most important deployment decision you’ll make.
Static vs server output
Astro builds in one of two output shapes, controlled by the output option in astro.config.mjs and by whether you’ve installed an adapter.
| Mode | output | Adapter needed | Produces | Best for |
|---|---|---|---|---|
| Static (default) | "static" | No | dist/ of HTML/CSS/JS | Blogs, docs, marketing, content sites |
| Server / SSR | "server" | Yes | Server entry + client assets | Auth, dashboards, per-request data |
Since Astro 5, you no longer need a global output: "server" just to render a few dynamic routes. With an adapter installed, individual pages opt into on-demand rendering with export const prerender = false, while the rest of the site stays static. This hybrid model is the recommended default for most apps.
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
// Pages are prerendered by default; opt specific ones into SSR.
adapter: node({ mode: "standalone" }),
site: "https://example.com",
});
---
// src/pages/dashboard.astro — rendered per request
export const prerender = false;
const user = await fetch(`https://api.example.com/me`, {
headers: { authorization: Astro.request.headers.get("authorization") ?? "" },
}).then((r) => r.json());
---
<h1>Welcome, {user.name}</h1>
Zero-JS-by-default still applies in both modes. Whether a page is static or server-rendered, Astro ships no client JavaScript unless a component carries a
client:*directive. The choice of output affects when HTML is generated, not how much script the browser downloads.
Building for production
Every deployment starts with the same command. It runs the build and writes the result to dist/.
npm run build
npm run preview # serve the production build locally to sanity-check it
Output:
20:14:02 [build] output: "static"
20:14:02 [build] Collecting build info...
20:14:03 [build] Completed in 842ms.
20:14:03 [build] Building static entrypoints...
20:14:04 [build] 12 page(s) built in 1.21s
20:14:04 [build] Complete!
The shape of dist/ differs by mode. Static builds contain only browseable assets. Server builds additionally emit a dist/server/ entry that the adapter wires into the host’s runtime.
Choosing an adapter for SSR
An adapter teaches Astro how to package the server output for a specific runtime. Install the one matching your host, then add it to the config.
| Adapter | Target | Install |
|---|---|---|
@astrojs/node | Self-hosted Node / Docker | npx astro add node |
@astrojs/vercel | Vercel serverless & edge | npx astro add vercel |
@astrojs/netlify | Netlify functions & edge | npx astro add netlify |
@astrojs/cloudflare | Cloudflare Pages & Workers | npx astro add cloudflare |
The astro add command installs the package, registers the adapter in astro.config.mjs, and updates dependencies in one step.
npx astro add vercel
After that, npm run build produces output in the format the platform expects — for Vercel and Netlify, the build is detected automatically when you connect the Git repository, with no extra configuration.
The site and base settings
Two config values matter for almost every deployment. site is your canonical production URL; Astro uses it to generate absolute URLs in sitemaps, RSS feeds, and canonical tags. base is the subpath your site is served from — essential when deploying to a subdirectory such as a GitHub Pages project site.
// astro.config.mjs
export default defineConfig({
site: "https://my-org.github.io",
base: "/my-repo", // only when not served from the domain root
});
---
// Build paths against base so links work under the subpath.
const base = import.meta.env.BASE_URL;
---
<a href={`${base}about`}>About</a>
Forgetting
siteis the most common deployment bug: sitemaps and RSS items silently emit relative orlocalhostURLs. Set it before your first production build.
Best practices
- Default to static output and opt individual routes into SSR with
prerender = false— only pull in an adapter when you genuinely need server logic. - Always set
site(andbasewhen serving from a subpath) before building for production so generated URLs are correct. - Run
npm run previewafter every build to catch broken links and missing assets before they reach your host. - Use
npx astro add <adapter>rather than hand-editing config; it pins compatible versions and avoids mismatches. - Keep secrets in environment variables read via
import.meta.env, never in committed config files. - Pin your Node version (an
.nvmrcorenginesfield) so the build host matches your local toolchain.