What production NestJS actually needs
NestJS gives you a clean, opinionated structure, but a production deployment needs more than npm run start. You want a small image, graceful shutdown so in-flight requests aren't dropped on redeploy, health endpoints for the orchestrator, validated config, and a real database wired in. Let's build all of that.
Step 1: Production-ready main.ts
Two non-negotiables: bind to the injected port and enable shutdown hooks.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.enableShutdownHooks(); // graceful SIGTERM handling
const port = process.env.PORT ?? 3000;
await app.listen(port, '0.0.0.0');
}
bootstrap();enableShutdownHooks() is what lets OnModuleDestroy / beforeApplicationShutdown run when the platform sends SIGTERM during a redeploy. Binding 0.0.0.0 is required inside containers.
Step 2: A lean multi-stage Dockerfile
Don't ship your devDependencies and source TypeScript to production.
# ---- build stage ----
FROM node:20.18-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# ---- runtime stage ----
FROM node:20.18-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]This keeps the runtime image to compiled JS plus production deps. On PandaStack you can also skip the Dockerfile entirely and let buildpacks auto-detect Node, but a Dockerfile gives you full control.
Step 3: Validate configuration at boot
Fail fast if config is wrong. Use @nestjs/config with a schema:
// app.module.ts
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DATABASE_URL: Joi.string().required(),
PORT: Joi.number().default(3000),
}),
}),
],
})
export class AppModule {}Now a missing DATABASE_URL crashes the boot loudly instead of erroring on the first query.
Step 4: Wire a managed PostgreSQL
With TypeORM or Prisma, read the connection string from the environment. On PandaStack, attaching a managed PostgreSQL (14.x or 16.x) to your app injects DATABASE_URL automatically — no copy-paste.
// TypeORM data source
import { DataSource } from 'typeorm';
export default new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
});Run migrations as a release step, not on every boot of every replica:
npm run typeorm migration:runStep 5: Add a health check
Use @nestjs/terminus so the orchestrator can tell when your app and its DB are healthy.
@Controller('health')
export class HealthController {
constructor(private health: HealthCheckService,
private db: TypeOrmHealthIndicator) {}
@Get()
@HealthCheck()
check() {
return this.health.check([() => this.db.pingCheck('database')]);
}
}A /health that pings the DB is far more useful than one that just returns 200.
Step 6: Deploy
Connect your Git repo in the PandaStack dashboard. On push, the build runs in an ephemeral Kubernetes Job pod with rootless BuildKit, the image goes to Google Artifact Registry, and Helm deploys it:
git push origin mainYou get live build and app logs (self-hosted Elasticsearch), automatic SSL on your custom domain, server-side metrics, rollbacks, and deploy history. If you set the start command explicitly, use node dist/main.js.
Step 7: Scale and resource sizing
NestJS is single-threaded per process. For more throughput, increase replicas (horizontal scaling) rather than relying on one big instance. Pick a compute tier matching your workload — PandaStack ranges from Free (0.25 CPU / 512MB) up to C2-2XCompute (8 CPU / 16GB), with compute-optimized (c1/c2) and memory-optimized (m1/m2) families.
Production checklist
- [ ] Binds to
$PORTon0.0.0.0 - [ ]
enableShutdownHooks()for graceful SIGTERM - [ ] Multi-stage Dockerfile, prod deps only
- [ ] Config validated at boot
- [ ] Migrations run as a release step
- [ ]
/healthpings the database - [ ] Replicas for throughput, right-sized tier
References
- [NestJS — Deployment](https://docs.nestjs.com/deployment)
- [NestJS — Terminus health checks](https://docs.nestjs.com/recipes/terminus)
- [TypeORM documentation](https://typeorm.io/)
- [Node.js Docker best practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md)
---
That's a NestJS app that survives redeploys, validates its own config, and reports its health. Push it to PandaStack's [free tier](https://dashboard.pandastack.io) — 5 web services and a managed Postgres at $0/mo — and watch DATABASE_URL wire itself in.