Back to Journal
DevOps

How to Build Zero-Downtime Deployments Using Nestjs

Step-by-step tutorial for building Zero-Downtime Deployments with Nestjs, from project setup through deployment.

Muneer Puthiya Purayil 19 min read

NestJS provides a structured framework for building Node.js services with built-in support for graceful shutdown, health checks via Terminus, and modular architecture that maps cleanly to zero-downtime deployment patterns. This tutorial covers building a NestJS application that deploys without any user-visible downtime, from module setup through Kubernetes configuration.

Project Setup

bash
1nest new zero-downtime-nestjs
2cd zero-downtime-nestjs
3npm install @nestjs/terminus @nestjs/schedule @prisma/client
4npm install -D prisma
5npx prisma init
6 

Graceful Shutdown Module

NestJS has built-in shutdown hook support:

typescript
1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4 
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7 
8 // Enable shutdown hooks
9 app.enableShutdownHooks();
10 
11 // Set global prefix
12 app.setGlobalPrefix('api');
13 
14 await app.listen(8080);
15 console.log('Application ready on port 8080');
16}
17bootstrap();
18 
typescript
1// shutdown/shutdown.service.ts
2import {
3 Injectable,
4 OnApplicationShutdown,
5 BeforeApplicationShutdown,
6} from '@nestjs/common';
7import { HealthService } from '../health/health.service';
8 
9@Injectable()
10export class ShutdownService
11 implements BeforeApplicationShutdown, OnApplicationShutdown
12{
13 private shuttingDown = false;
14 
15 constructor(private readonly healthService: HealthService) {}
16 
17 get isShuttingDown(): boolean {
18 return this.shuttingDown;
19 }
20 
21 async beforeApplicationShutdown(signal?: string) {
22 console.log(`Received shutdown signal: ${signal}`);
23 
24 // Phase 1: Mark as unhealthy
25 this.shuttingDown = true;
26 this.healthService.setReady(false);
27 
28 // Phase 2: Wait for load balancer to deregister
29 await this.sleep(15000);
30 console.log('LB deregistration wait complete');
31 }
32 
33 async onApplicationShutdown(signal?: string) {
34 console.log('Application shutdown complete');
35 }
36 
37 private sleep(ms: number): Promise<void> {
38 return new Promise((resolve) => setTimeout(resolve, ms));
39 }
40}
41 

Health Check Module with Terminus

typescript
1// health/health.module.ts
2import { Module } from '@nestjs/common';
3import { TerminusModule } from '@nestjs/terminus';
4import { HealthController } from './health.controller';
5import { HealthService } from './health.service';
6import { PrismaHealthIndicator } from './prisma.health';
7import { RedisHealthIndicator } from './redis.health';
8 
9@Module({
10 imports: [TerminusModule],
11 controllers: [HealthController],
12 providers: [
13 HealthService,
14 PrismaHealthIndicator,
15 RedisHealthIndicator,
16 ],
17 exports: [HealthService],
18})
19export class HealthModule {}
20 
typescript
1// health/health.controller.ts
2import { Controller, Get } from '@nestjs/common';
3import {
4 HealthCheck,
5 HealthCheckService,
6} from '@nestjs/terminus';
7import { HealthService } from './health.service';
8import { PrismaHealthIndicator } from './prisma.health';
9import { RedisHealthIndicator } from './redis.health';
10 
11@Controller('health')
12export class HealthController {
13 constructor(
14 private health: HealthCheckService,
15 private healthService: HealthService,
16 private prismaHealth: PrismaHealthIndicator,
17 private redisHealth: RedisHealthIndicator,
18 ) {}
19 
20 @Get('ready')
21 @HealthCheck()
22 async readiness() {
23 if (!this.healthService.isReady) {
24 throw new ServiceUnavailableException('Not ready');
25 }
26 
27 return this.health.check([
28 () => this.prismaHealth.isHealthy('database'),
29 () => this.redisHealth.isHealthy('redis'),
30 ]);
31 }
32 
33 @Get('live')
34 liveness() {
35 return { status: 'alive' };
36 }
37}
38 
typescript
1// health/health.service.ts
2import { Injectable } from '@nestjs/common';
3 
4@Injectable()
5export class HealthService {
6 private ready = false;
7 
8 get isReady(): boolean {
9 return this.ready;
10 }
11 
12 setReady(ready: boolean) {
13 this.ready = ready;
14 }
15}
16 
typescript
1// health/prisma.health.ts
2import { Injectable } from '@nestjs/common';
3import {
4 HealthIndicator,
5 HealthIndicatorResult,
6 HealthCheckError,
7} from '@nestjs/terminus';
8import { PrismaService } from '../prisma/prisma.service';
9 
10@Injectable()
11export class PrismaHealthIndicator extends HealthIndicator {
12 constructor(private prisma: PrismaService) {
13 super();
14 }
15 
16 async isHealthy(key: string): Promise<HealthIndicatorResult> {
17 try {
18 await this.prisma.$queryRaw`SELECT 1`;
19 return this.getStatus(key, true);
20 } catch (error) {
21 throw new HealthCheckError(
22 'Database check failed',
23 this.getStatus(key, false, { error: error.message }),
24 );
25 }
26 }
27}
28 

Request Tracking Interceptor

typescript
1// interceptors/request-tracker.interceptor.ts
2import {
3 Injectable,
4 NestInterceptor,
5 ExecutionContext,
6 CallHandler,
7} from '@nestjs/common';
8import { Observable, tap } from 'rxjs';
9 
10@Injectable()
11export class RequestTrackerService {
12 private activeCount = 0;
13 
14 increment() {
15 this.activeCount++;
16 }
17 
18 decrement() {
19 this.activeCount--;
20 }
21 
22 get active(): number {
23 return this.activeCount;
24 }
25 
26 async waitForDrain(timeoutMs = 30000): Promise<boolean> {
27 const deadline = Date.now() + timeoutMs;
28 while (Date.now() < deadline) {
29 if (this.activeCount === 0) return true;
30 await new Promise((r) => setTimeout(r, 100));
31 }
32 return this.activeCount === 0;
33 }
34}
35 
36@Injectable()
37export class RequestTrackerInterceptor implements NestInterceptor {
38 constructor(private tracker: RequestTrackerService) {}
39 
40 intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
41 this.tracker.increment();
42 return next.handle().pipe(
43 tap({
44 complete: () => this.tracker.decrement(),
45 error: () => this.tracker.decrement(),
46 }),
47 );
48 }
49}
50 

Shutdown Guard Middleware

Reject requests during shutdown:

typescript
1// middleware/shutdown-guard.middleware.ts
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4import { ShutdownService } from '../shutdown/shutdown.service';
5 
6@Injectable()
7export class ShutdownGuardMiddleware implements NestMiddleware {
8 constructor(private shutdownService: ShutdownService) {}
9 
10 use(req: Request, res: Response, next: NextFunction) {
11 if (this.shutdownService.isShuttingDown) {
12 if (!req.path.startsWith('/health')) {
13 res.setHeader('Connection', 'close');
14 res.setHeader('Retry-After', '5');
15 return res.status(503).json({
16 error: 'Service shutting down',
17 });
18 }
19 }
20 next();
21 }
22}
23 

Need a second opinion on your DevOps pipelines architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Feature Flag Service

typescript
1// features/feature-flag.service.ts
2import { Injectable, OnModuleInit } from '@nestjs/common';
3import { Cron, CronExpression } from '@nestjs/schedule';
4import { createHash } from 'crypto';
5import { RedisService } from '../redis/redis.service';
6 
7interface FeatureFlag {
8 key: string;
9 enabled: boolean;
10 rolloutPercent: number;
11 allowedTenants?: string[];
12}
13 
14@Injectable()
15export class FeatureFlagService implements OnModuleInit {
16 private flags = new Map<string, FeatureFlag>();
17 
18 constructor(private redis: RedisService) {}
19 
20 async onModuleInit() {
21 await this.refreshFlags();
22 }
23 
24 isEnabled(key: string, tenantId: string): boolean {
25 const flag = this.flags.get(key);
26 if (!flag?.enabled) return false;
27 
28 if (flag.allowedTenants?.includes(tenantId)) return true;
29 
30 const hash = createHash('md5')
31 .update(`${key}:${tenantId}`)
32 .digest('hex');
33 return parseInt(hash.slice(0, 8), 16) % 100 < flag.rolloutPercent;
34 }
35 
36 @Cron(CronExpression.EVERY_10_SECONDS)
37 async refreshFlags() {
38 const keys = await this.redis.keys('flag:*');
39 for (const k of keys) {
40 const raw = await this.redis.get(k);
41 if (raw) {
42 const flag: FeatureFlag = JSON.parse(raw);
43 this.flags.set(flag.key, flag);
44 }
45 }
46 }
47}
48 

Database Migrations

Separate migrations from deployment using Prisma:

typescript
1// prisma/migrate.ts
2import { execSync } from 'child_process';
3 
4async function main() {
5 console.log('Running Prisma migrations...');
6 execSync('npx prisma migrate deploy', { stdio: 'inherit' });
7 console.log('Migrations complete');
8}
9 
10main().catch((e) => {
11 console.error('Migration failed:', e);
12 process.exit(1);
13});
14 
yaml
1# CI/CD: migrations run before deployment
2- name: Run migrations
3 run: npx ts-node prisma/migrate.ts
4 env:
5 DATABASE_URL: ${{ secrets.DATABASE_URL }}
6 
7- name: Deploy
8 run: kubectl set image deployment/api api=$IMAGE:$TAG
9 

Cache Warming on Startup

typescript
1// warmup/warmup.service.ts
2import { Injectable, OnModuleInit } from '@nestjs/common';
3import { PrismaService } from '../prisma/prisma.service';
4import { RedisService } from '../redis/redis.service';
5import { HealthService } from '../health/health.service';
6 
7@Injectable()
8export class WarmupService implements OnModuleInit {
9 constructor(
10 private prisma: PrismaService,
11 private redis: RedisService,
12 private health: HealthService,
13 ) {}
14 
15 async onModuleInit() {
16 await this.warmCaches();
17 this.health.setReady(true);
18 }
19 
20 private async warmCaches() {
21 const [plans, configs] = await Promise.all([
22 this.prisma.plan.findMany({ where: { active: true } }),
23 this.prisma.config.findMany(),
24 ]);
25 
26 const pipeline = this.redis.pipeline();
27 for (const plan of plans) {
28 pipeline.set(`plan:${plan.id}`, JSON.stringify(plan), 'EX', 3600);
29 }
30 for (const config of configs) {
31 pipeline.set(`config:${config.key}`, config.value, 'EX', 7200);
32 }
33 await pipeline.exec();
34 
35 console.log(`Warmed ${plans.length} plans, ${configs.length} configs`);
36 }
37}
38 

Kubernetes Deployment

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: nestjs-api
5spec:
6 replicas: 3
7 strategy:
8 type: RollingUpdate
9 rollingUpdate:
10 maxSurge: 1
11 maxUnavailable: 0
12 template:
13 spec:
14 terminationGracePeriodSeconds: 45
15 containers:
16 - name: api
17 image: nestjs-api:latest
18 ports:
19 - containerPort: 8080
20 readinessProbe:
21 httpGet:
22 path: /health/ready
23 port: 8080
24 initialDelaySeconds: 10
25 periodSeconds: 5
26 failureThreshold: 3
27 livenessProbe:
28 httpGet:
29 path: /health/live
30 port: 8080
31 initialDelaySeconds: 15
32 periodSeconds: 10
33 lifecycle:
34 preStop:
35 exec:
36 command: ["/bin/sh", "-c", "sleep 15"]
37 env:
38 - name: NODE_ENV
39 value: production
40 resources:
41 requests:
42 memory: "256Mi"
43 cpu: "250m"
44 limits:
45 memory: "512Mi"
46 cpu: "500m"
47 

App Module Assembly

typescript
1// app.module.ts
2import {
3 Module,
4 NestModule,
5 MiddlewareConsumer,
6} from '@nestjs/common';
7import { ScheduleModule } from '@nestjs/schedule';
8import { HealthModule } from './health/health.module';
9import { ShutdownService } from './shutdown/shutdown.service';
10import { ShutdownGuardMiddleware } from './middleware/shutdown-guard.middleware';
11import { RequestTrackerService } from './interceptors/request-tracker.interceptor';
12import { FeatureFlagService } from './features/feature-flag.service';
13import { WarmupService } from './warmup/warmup.service';
14import { PrismaModule } from './prisma/prisma.module';
15import { RedisModule } from './redis/redis.module';
16 
17@Module({
18 imports: [
19 ScheduleModule.forRoot(),
20 HealthModule,
21 PrismaModule,
22 RedisModule,
23 ],
24 providers: [
25 ShutdownService,
26 RequestTrackerService,
27 FeatureFlagService,
28 WarmupService,
29 ],
30})
31export class AppModule implements NestModule {
32 configure(consumer: MiddlewareConsumer) {
33 consumer
34 .apply(ShutdownGuardMiddleware)
35 .forRoutes('*');
36 }
37}
38 

FAQ

Need expert help?

Building with CI/CD pipelines?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026