Back to Blog
Tutorial11 min read2026-06-30

How to Deploy a NestJS App to Production (2026 Guide)

A production-grade NestJS deployment: a tight multi-stage Dockerfile, graceful shutdown, health checks, config validation, and an auto-wired managed Postgres. From git push to live in minutes.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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:run

Step 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 main

You 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 $PORT on 0.0.0.0
  • [ ] enableShutdownHooks() for graceful SIGTERM
  • [ ] Multi-stage Dockerfile, prod deps only
  • [ ] Config validated at boot
  • [ ] Migrations run as a release step
  • [ ] /health pings 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.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also