Node Adapter
Astro ships zero JavaScript by default, but the moment you need server-rendered pages, API endpoints, or per-request logic, you need a runtime to execute that server code. The @astrojs/node adapter lets you run a server-rendered (SSR) Astro app on any Node.js host — a VPS, Docker container, bare-metal box, or any platform that can run node. It is the most flexible deployment target because it makes no assumptions about a specific cloud provider, so if you control your own infrastructure, this is the adapter you want.
Installing the adapter
The fastest way to add the adapter is with the astro add command, which installs the package, updates astro.config.mjs, and wires up the SSR output mode for you.
npx astro add node
If you prefer to do it manually, install the package and configure it yourself.
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",
}),
});
The mode option is required and determines how the adapter exposes your app. Both modes produce a server build under dist/server/ and a client build under dist/client/.
The adapter only affects pages and endpoints that opt into server rendering. With
output: "server", pages render on demand by default; addexport const prerender = true;to a page to render it statically at build time. Useoutput: "static"only if you have no SSR at all.
Standalone vs middleware mode
The single most important decision is which mode to use. The two modes serve different deployment shapes.
| Mode | What it produces | Serves static assets | Use when |
|---|---|---|---|
standalone | A complete, ready-to-run HTTP server | Yes (built in) | You run node ./dist/server/entry.mjs directly |
middleware | An Express/Connect-style request handler | No (you serve them) | You embed Astro inside an existing Node server |
Standalone mode
Standalone mode is the simplest path to production. The build emits a self-contained server that listens on a port, serves your static assets, and renders SSR routes — no extra framework needed.
node ./dist/server/entry.mjs
Output:
Server listening on http://127.0.0.1:4321
You control the host and port through environment variables, which is convenient inside containers and process managers.
HOST=0.0.0.0 PORT=8080 node ./dist/server/entry.mjs
Middleware mode
Middleware mode is for teams that already run an HTTP server and want to mount Astro as one piece of it. The adapter exports a handler you plug into Express, Fastify (via middleware), or the raw node:http module.
// server.mjs
import express from "express";
import { handler as ssrHandler } from "./dist/server/entry.mjs";
const app = express();
// Serve Astro's built client assets yourself in middleware mode.
app.use(express.static("dist/client/"));
// Hand everything else to the Astro SSR handler.
app.use(ssrHandler);
app.listen(3000, () => {
console.log("App running on http://localhost:3000");
});
Because you own the server, you can add custom Express routes, authentication middleware, or proxy logic before or after the Astro handler. You can also pass locals into Astro by calling ssrHandler(req, res, next, locals) and reading them via Astro.locals in your pages.
Accessing the request and response
SSR pages and endpoints receive the full standard Request object, so per-request logic works the same as in any modern web framework.
---
// src/pages/account.astro
const cookie = Astro.request.headers.get("cookie");
const user = cookie ? await getUserFromCookie(cookie) : null;
if (!user) {
return Astro.redirect("/login");
}
---
<h1>Welcome back, {user.name}</h1>
API endpoints work identically and return a Response.
// src/pages/api/health.ts
import type { APIRoute } from "astro";
export const GET: APIRoute = () =>
new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
Dockerizing a standalone app
A standalone build pairs naturally with a small Node container. Build the app, then copy dist/ and node_modules/ into a runtime image.
# Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
Run a reverse proxy such as Nginx or Caddy in front of the standalone server for TLS termination, compression, and HTTP/2. The Node server focuses on rendering; let the proxy handle the edge concerns.
Best practices
- Prefer
standalonemode unless you genuinely need to embed Astro inside an existing Node server — it is simpler and serves static assets for you. - Keep SSR surface area small: mark pages that do not need per-request data with
export const prerender = true;so they ship as static HTML and stay zero-JS. - Configure
HOSTandPORTvia environment variables rather than hardcoding them, so the same build runs locally, in Docker, and in production. - In middleware mode, always serve
dist/client/yourself — the adapter will not, and missing assets are a common deployment bug. - Put a reverse proxy in front for TLS, gzip/brotli, and caching of static assets instead of doing it in Node.
- Use a process manager (PM2, systemd, or your container orchestrator) to restart the server on crashes and across deploys.