Back to Blog
Tutorial9 min read2026-07-01

How to Deploy a NestJS App to the Cloud

Deploy a NestJS API to the cloud the right way: a multi-stage Docker build, proper production config, graceful shutdown, health checks, and a managed database wired in automatically.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

NestJS gives you structure out of the box — modules, dependency injection, decorators — but it doesn't tell you how to ship it. This guide covers a clean, production-ready deployment: a lean Docker image, the right build pipeline, graceful shutdown, and a managed database.

Build vs. runtime: don't ship your toolchain

NestJS compiles TypeScript to JavaScript. Your production image only needs the compiled dist/ folder and production dependencies — not @nestjs/cli, not TypeScript, not your dev dependencies. A multi-stage Docker build keeps the final image small and the attack surface minimal.

# --- build stage ---
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- runtime stage ---
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]

The --omit=dev flag in the runtime stage drops dev dependencies, often halving image size.

Production-ready bootstrap

Your main.ts should bind to 0.0.0.0 (not localhost, which won't accept external traffic inside a container), read the port from the environment, and enable graceful shutdown.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  app.enableCors();
  const port = process.env.PORT ?? 3000;
  await app.listen(port, '0.0.0.0');
}
bootstrap();

enableShutdownHooks() lets Nest's lifecycle events fire on SIGTERM, so you can close database connections and finish in-flight requests during a rolling deploy.

A managed database with TypeORM or Prisma

Whichever ORM you use, read the connection string from the environment. Here's TypeORM:

import { TypeOrmModule } from '@nestjs/typeorm';

TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  autoLoadEntities: true,
  synchronize: false, // NEVER true in production
});

Never set synchronize: true in production. It auto-alters your schema based on entities and can silently drop columns. Use migrations:

npm run typeorm migration:run

Run migrations as a release step before the new code goes live, using the expand/contract pattern (add columns first, remove them in a later release).

Health checks with Terminus

NestJS ships an official health-check module, @nestjs/terminus:

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([() => this.db.pingCheck('database')]);
  }
}

Wire this endpoint into your platform's readiness probe so traffic only routes to healthy instances.

Deploying on PandaStack

PandaStack auto-detects Node projects and runs your build automatically. The flow:

  1. 1Create a PostgreSQL database in the dashboard — DATABASE_URL is injected into your service automatically.
  2. 2Connect your Git repo as a container app. PandaStack detects your Dockerfile, or uses buildpacks if you don't have one (it knows npm run build then node dist/main.js).
  3. 3Set env vars like JWT_SECRET in the dashboard.
  4. 4Push to your branch — it builds and deploys with live logs.

Builds run in rootless BuildKit inside ephemeral Kubernetes Jobs, images go to Artifact Registry, and deploys happen via Helm. You get rollbacks, deploy history, automatic SSL, and server-side metrics without adding an SDK.

SettingValue
Install commandnpm ci (auto)
Build commandnpm run build (auto)
Start commandnode dist/main.js
Port3000 (from PORT)
DatabaseManaged PostgreSQL (DATABASE_URL)

Things that bite people

  • Binding to localhost — the container can't receive external traffic. Always 0.0.0.0.
  • Forgetting NODE_ENV=production — disables certain optimizations and can leave dev-only behavior on.
  • synchronize: true — fine for prototyping, dangerous in production.
  • Not enabling shutdown hooks — abrupt termination drops requests and leaks DB connections.

References

  • NestJS deployment guide: https://docs.nestjs.com/deployment
  • NestJS Terminus health checks: https://docs.nestjs.com/recipes/terminus
  • TypeORM migrations: https://typeorm.io/migrations
  • Node.js Docker best practices: https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md
  • Docker multi-stage builds: https://docs.docker.com/build/building/multi-stage/

---

PandaStack's free tier includes container apps, a managed database, and automatic SSL — connect your NestJS repo and it builds, deploys, and wires the database in for you. Try it at https://dashboard.pandastack.io

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also