Back to Blog
Tutorial11 min read2026-07-02

How to Deploy an Astro SSR App with a Database

Astro's server-side rendering mode with the Node adapter lets you build dynamic, database-backed sites. Learn how to deploy an Astro SSR app to production with PostgreSQL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Astro is best known for shipping zero JavaScript by default, but it's equally capable as a server-rendered framework. Switch on SSR with an adapter and you can build dynamic, database-backed pages and API endpoints. This guide deploys an Astro SSR app with a managed PostgreSQL database.

Static vs SSR vs hybrid

Astro supports three output modes:

  • static — prerender everything at build time (the default; deploy as a static site).
  • server — render on every request (full SSR; needs a server runtime).
  • hybrid — prerender most pages but mark some as server-rendered with export const prerender = false.

For database-backed dynamic pages you need server or hybrid. We'll use server with the Node adapter.

Configuring the Node adapter

Install and configure the adapter:

npx astro add node

This updates astro.config.mjs:

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});

The standalone mode produces a self-contained Node server you run directly. (The alternative, middleware mode, expects you to wire it into an existing Express/Connect app.)

Building

npm run build

With the standalone Node adapter, this emits a server entry at dist/server/entry.mjs. The server reads HOST and PORT from the environment:

HOST=0.0.0.0 PORT=4321 node ./dist/server/entry.mjs

Database-backed pages

Link a managed PostgreSQL database so DATABASE_URL is injected, then query it directly in a server-rendered page or an API route. Astro pages run on the server in SSR mode:

---
// src/pages/products.astro
import postgres from 'postgres';
const sql = postgres(import.meta.env.DATABASE_URL);
const products = await sql`SELECT id, name, price FROM products`;
---
<ul>
  {products.map((p) => <li>{p.name} — ${p.price}</li>)}
</ul>

For a connection pool reused across requests, initialize the client in a shared module rather than per page:

// src/lib/db.ts
import postgres from 'postgres';
export const sql = postgres(import.meta.env.DATABASE_URL);

API endpoints

Astro endpoints in src/pages/api/ give you JSON routes alongside your pages:

// src/pages/api/health.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = () => new Response('ok', { status: 200 });

Point the platform's readiness probe at /api/health.

The Dockerfile

# ---- Build ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Runtime ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

Unlike Nitro-based frameworks, the Astro Node adapter doesn't bundle node_modules, so we copy them into the runtime stage. If image size matters, prune dev dependencies with npm prune --omit=dev after the build.

Environment variables and secrets

VariablePurpose
NODE_ENVproduction
HOST0.0.0.0
PORTinjected listen port
DATABASE_URLinjected by managed DB link

Astro exposes env vars to the client only when prefixed with PUBLIC_. Keep DATABASE_URL unprefixed so it stays server-side. Access server secrets via import.meta.env in .astro frontmatter or endpoints — they're stripped from the client bundle.

Hybrid: mixing static and dynamic

If most of your site is static (marketing pages) and only a few routes need the database, use output: 'server' plus export const prerender = true on the static pages. Astro prerenders those at build time and renders the rest on demand — the best of both worlds for cost and performance.

Deploying

Push, connect the repo, link a managed PostgreSQL database, set HOST and NODE_ENV, and deploy. The platform builds the Dockerfile (or auto-detects the framework) and runs the standalone Node server. Live build logs surface any SSR build issues; app logs confirm the server bound to the injected port.

git push origin main

Conclusion

Astro SSR deploys as a standalone Node server once you add the Node adapter in standalone mode, build, and run the emitted entry.mjs binding to 0.0.0.0 and the injected port. Server-rendered pages and endpoints read DATABASE_URL directly, and hybrid mode lets you prerender the static majority for free.

Try an Astro SSR app plus a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database auto-wires via DATABASE_URL.

References

  • [Astro: On-demand Rendering (SSR)](https://docs.astro.build/en/guides/on-demand-rendering/)
  • [Astro Node.js Adapter](https://docs.astro.build/en/guides/integrations-guide/node/)
  • [Astro: Endpoints](https://docs.astro.build/en/guides/endpoints/)
  • [Astro: Environment Variables](https://docs.astro.build/en/guides/environment-variables/)
  • [postgres.js Client](https://github.com/porsager/postgres)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also