AnalogJS is to Angular what Next.js is to React and Nuxt is to Vue — a meta-framework that adds file-based routing, server-side rendering, API routes, and a Vite-powered dev experience on top of Angular. Because Analog builds on Nitro (the same server engine behind Nuxt), it can target many runtimes. This guide deploys an Analog full-stack app as a Node server in a container.
How Analog builds
Analog uses Vite for the client build and Nitro for the server. A production build produces:
- A client bundle (the browser app).
- A server bundle (Nitro output) that handles SSR and API routes.
The default Nitro preset for self-hosting is node-server, which emits a standalone Node server you run with node.
Building for a Node server
Run the build:
npm run buildThis produces dist/analog/public (static client assets) and dist/analog/server (the Nitro server). With the node-server preset, the server entry is dist/analog/server/index.mjs. To force the preset, set:
NITRO_PRESET=node-server npm run buildor configure it in vite.config.ts under the Analog plugin's nitro options.
The production server
The built Nitro server reads PORT and HOST from the environment, so it binds correctly inside a container:
HOST=0.0.0.0 PORT=3000 node dist/analog/server/index.mjsNitro respects these env vars out of the box — no extra config to listen on the injected port.
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
# Copy only the build output
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/analog/server/index.mjs"]Because Nitro bundles dependencies into the server output, the runtime stage doesn't need node_modules — a big image-size win. Verify your build output path; some Analog versions emit to dist/analog/server and others to .output/server depending on the Nitro version.
API routes
Analog's file-based API routes live in src/server/routes/ and are served by the same Nitro server:
// src/server/routes/api/v1/hello.ts
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return { message: 'Hello from Analog API' };
});These run server-side in the same process as SSR, so a single container serves both your rendered Angular app and your API. No separate backend needed for simple full-stack apps.
Adding a database
For data, connect to a managed database from your API routes. Link a managed PostgreSQL instance so DATABASE_URL is injected, then use a Postgres client (e.g. postgres or pg) inside an API route:
import { defineEventHandler } from 'h3';
import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL!);
export default defineEventHandler(async () => {
const users = await sql`SELECT id, name FROM users LIMIT 10`;
return { users };
});Initialize the client once at module scope so the connection pool is reused across requests rather than created per request.
Environment variables
| Variable | Purpose |
|---|---|
NODE_ENV | production |
HOST | 0.0.0.0 |
PORT | injected listen port |
DATABASE_URL | injected by managed DB link |
Server-only secrets (like DATABASE_URL) must never be exposed to the client bundle. Analog/Vite only inlines variables prefixed for the client (e.g. VITE_), so keep secrets unprefixed — they stay server-side.
Health checks
Add an API route that returns 200 and point the platform's readiness probe at it:
// src/server/routes/health.ts
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'ok');Deploying
Push the repo, connect it, set HOST and NODE_ENV, link a managed PostgreSQL if you need data, and deploy. The platform builds the Dockerfile (or auto-detects Node), and serves SSR + API from a single container. Live build logs show any Vite or Nitro build errors; app logs confirm the server bound to the injected port.
git push origin mainSSR vs static
If your Analog app is fully static (no SSR, no API routes), you can prerender it and deploy as a static site instead of a Node server — cheaper and faster to serve. But the moment you need API routes or per-request SSR, the Node server deployment above is the right model.
Conclusion
Analog deploys as a self-contained Nitro Node server: build with the node-server preset, run the emitted index.mjs binding to 0.0.0.0 and the injected port, serve SSR and API routes from one container, and read DATABASE_URL server-side for data. Nitro's bundled output keeps the runtime image tiny.
Try an Analog app plus a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database wires itself in.
References
- [AnalogJS Documentation](https://analogjs.org/docs)
- [AnalogJS: Deployment](https://analogjs.org/docs/features/deployment/overview)
- [Nitro: Node Server Preset](https://nitro.build/deploy/runtimes/node)
- [AnalogJS: Server / API Routes](https://analogjs.org/docs/features/api/overview)
- [postgres.js Client](https://github.com/porsager/postgres)