Skip to content
Astro as integrations 4 min read

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; add export const prerender = true; to a page to render it statically at build time. Use output: "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.

ModeWhat it producesServes static assetsUse when
standaloneA complete, ready-to-run HTTP serverYes (built in)You run node ./dist/server/entry.mjs directly
middlewareAn Express/Connect-style request handlerNo (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 standalone mode 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 HOST and PORT via 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.
Last updated June 14, 2026
Was this helpful?