ProductionDeployment

Node.js

Deploy a NextRush app to Node.js using @nextrush/adapter-node.

@nextrush/adapter-node connects a NextRush Application to Node's built-in http.createServer. It's the default target — the nextrush meta package re-exports it directly, so import { listen } from 'nextrush' already runs on Node.js with no extra install.

Minimal deployment

src/index.ts
import { createApp, createRouter, listen } from 'nextrush';

const app = createApp();
const router = createRouter();

router.get('/health', (ctx) => ctx.json({ status: 'ok' }));
app.route('/', router);

await listen(app, Number(process.env.PORT ?? 8080));
node dist/index.js

listen() is a thin wrapper: it calls serve() and logs a startup line through app.logger. serve() itself calls app.ready() (boots any registered extensions), creates the Node.js server, and resolves once it's bound.

No log without a logger

createApp()'s default logger is a no-op — listen()'s startup message and any ctx/app.logger calls produce no output until you attach a real logger (e.g. @nextrush/logger) via createApp({ logger }). Don't assume you'll see console output in production without configuring one.

serve() options that matter in production

@nextrush/adapter-node's serve(app, options) accepts:

ServeOptions (@nextrush/adapter-node)

PropertyTypeDescription
portnumber= 8080Port to listen on.
hoststring= '0.0.0.0'Host/interface to bind to.
timeoutnumber= 30000Per-request socket timeout in ms (`server.timeout`).
keepAliveTimeoutnumber= 5000Keep-alive timeout in ms (`server.keepAliveTimeout`).
shutdownTimeoutnumber= 30000Grace period to drain in-flight requests during `close()` before force-closing.
onListen?(info: { port: number; host: string }) => voidCalled once the server is bound.
onError?(error: Error) => voidCustom handler for uncaught server-level errors.
src/index.ts
import { createApp, createRouter } from 'nextrush';
import { serve } from '@nextrush/adapter-node';

const app = createApp();
const router = createRouter();
router.get('/health', (ctx) => ctx.json({ status: 'ok' }));
app.route('/', router);

const server = await serve(app, {
  port: Number(process.env.PORT ?? 8080),
  host: '0.0.0.0',
  onListen: ({ port, host }) => {
    console.log(`listening on ${host}:${port}`);
  },
});

process.on('SIGTERM', async () => {
  await server.close(); // drains in-flight requests, then tears down extensions
  process.exit(0);
});

server.close() stops accepting new connections, waits up to shutdownTimeout for in-flight requests to drain, then calls app.close() for extension teardown. See Reliability for the full graceful-shutdown pattern, including which signals to handle and how health checks should react during drain.

Behind a reverse proxy

If NextRush sits behind nginx, an ALB, or a platform load balancer, enable proxy: true so ctx.ip reads X-Forwarded-For from a trusted upstream instead of the raw socket address:

const app = createApp({ proxy: true });

Only trust proxies you control

proxy: true makes NextRush trust client-supplied X-Forwarded-For headers. Enabling it behind an untrusted or misconfigured proxy lets clients spoof their own IP — only set this when a proxy you control is guaranteed to overwrite that header on every request.

Running under a process manager

A raw node dist/index.js process won't restart itself after a crash and won't survive a reboot. Two common approaches:

/etc/systemd/system/nextrush-app.service
[Unit]
Description=NextRush application
After=network.target

[Service]
Type=simple
User=nextrush
WorkingDirectory=/opt/nextrush-app
Environment=NODE_ENV=production
Environment=PORT=8080
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=2
# systemd sends SIGTERM by default — matches the shutdown handler above.
KillSignal=SIGTERM
TimeoutStopSec=35

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now nextrush-app
sudo systemctl status nextrush-app
pm2 start dist/index.js --name nextrush-app
pm2 save
pm2 startup   # generates a boot script for your init system

See Scaling — process managers for when to use PM2's cluster mode versus letting a container orchestrator manage replicas instead — don't run both at once.

On a container platform, skip the process manager

If you're deploying to Kubernetes, ECS, or Cloud Run, the orchestrator already restarts crashed containers and manages replica count. Run one process per container (see Docker) instead of adding PM2/systemd inside it.

Environment setup

# .env (never commit this file)
NODE_ENV=production
PORT=8080
const PORT = Number(process.env.PORT ?? 8080);

NODE_ENV=production is read by createApp() to decide default error-response verbosity — see Configuration for the full list of environment-driven defaults and how to externalize secrets.

Requirements

  • Node.js >= 22.0.0 (declared in @nextrush/adapter-node's package.json engines field)
  • Docker — containerize this same app
  • Reliability — graceful shutdown, health checks, timeouts
  • Configuration — environment variables and secrets
Was this helpful?

On this page