NestJS has a deployment quirk that plain Express projects do not have: TypeScript needs to compile before the container starts, and the production image should not carry the compiler or .ts source files. Many teams solve this with 1GB images or a Dockerfile that copies everything inside. You can do better.
This tutorial covers the full path, from a multi-stage Dockerfile to a live service with HTTPS on Guara Cloud, running on infrastructure in Brazil.
Quick answer
To deploy a NestJS application in Brazil, compile TypeScript in a build stage of the Dockerfile, copy only the dist directory and production dependencies into the final image, listen on the port set by PORT via environment variable, and publish the container on Guara Cloud. The platform handles HTTPS, public domain, logs, and bills in BRL.
Key takeaways
- Use a multi-stage Dockerfile. The build stage compiles TypeScript. The final stage carries only
dist/and productionnode_modules. - Read
PORTfrom the environment. Do not hardcode 3000. - Use the NestJS
ConfigModuleto read environment variables with typing. - Set secrets from the Guara Cloud dashboard, never in the repository.
- Add a health check with
@nestjs/terminusso the platform knows when the service is ready.
When this tutorial applies
Use this flow for REST APIs, GraphQL services (with Apollo or Mercurius), and microservices built with NestJS. If your application uses a queue (BullMQ), websockets (Gateway), or gRPC, the container works the same way. The differences are which ports you expose and which environment variables you set.
When not to use this flow
If your application is purely a worker with no HTTP port (only consuming a queue), the deploy is similar but you skip the HTTP health check. If the project uses serverless (Lambda, Cloud Functions), this guide does not apply. For Nx monorepos where multiple NestJS apps coexist, adjust the build paths in the Dockerfile to point to the correct app.
Before you start
- A NestJS project created with @nestjs/cli (nest new my-app)
- Node.js 20+ installed locally for testing
- Docker installed to validate the image
- A Guara Cloud account
1. Configure the port via environment variable
NestJS defaults to 3000. In a container, the platform defines the port. Open src/main.ts and adjust:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = Number(process.env.PORT ?? 3000);
await app.listen(port, '0.0.0.0');
}
bootstrap(); The 0.0.0.0 is mandatory. Without it, the application listens only on loopback and the load balancer cannot reach the container.
2. Multi-stage Dockerfile for NestJS
This is the part that separates a 50MB image from an 800MB one. The first stage installs everything, compiles TypeScript, and optionally runs tests. The second stage copies only what production needs.
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
CMD ["node", "dist/main.js"] This Dockerfile produces roughly an 80MB image. The npm ci --omit=dev in the final stage keeps TypeScript, ESLint, and Jest out of production.
3. Set up environment variables with ConfigModule
NestJS has an official module for typing and validating environment variables. If you have not installed it yet:
npm install @nestjs/config Then register it in the AppModule:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
],
})
export class AppModule {} In the Guara Cloud dashboard, configure the variables your application consumes:
Common environment variables
| Name | Value |
|---|---|
NODE_ENV | production |
PORT | 3000 |
DATABASE_URL | postgres://user:***@host:5432/mydb |
JWT_SECRET | long-random-string |
Never commit .env to the repository. Use .env.example as a reference with placeholder values.
4. Add a health check
Guara Cloud uses HTTP probes to decide if a container is healthy. Install Terminus and create a simple endpoint:
npm install @nestjs/terminus import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HealthCheck, HttpHealthIndicator } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.http.pingCheck('ping', 'https://www.google.com'),
]);
}
} For something simpler (no external dependencies), just return { status: 'ok' } at /health. The platform only needs a 200 OK.
5. Deploy on Guara Cloud
With the repository on GitHub and the Dockerfile at the root, the process on Guara Cloud is:
Dashboard walkthrough
- Create a new service and connect your GitHub repository
- The platform detects the Dockerfile automatically
- Confirm the HTTP port (the PORT value, usually 3000)
- Add environment variables through the dashboard
- Start the deploy and watch the build logs in real time
The build runs the full Dockerfile: installs dependencies, compiles TypeScript, generates the final image. The first deploy takes 2 to 4 minutes depending on project size. Subsequent deploys are faster because of Docker layer caching.
6. Validate the service in production
After the deploy completes, check:
- The public URL responds with 200.
- The
/healthendpoint returns ok status. - Logs show the application listening on the correct port.
- CPU and memory usage are within expectations (in the Guara Cloud dashboard, observability tab).
If something fails, build and runtime logs are available in the dashboard. Most problems surface in the first few seconds.
Common issues
- Problem The container starts and exits immediately
- Solution Check that main.ts listens on 0.0.0.0 and uses process.env.PORT. Run "docker run -e PORT=3000 -p 3000:3000 my-image" locally to reproduce.
- Problem Cannot find module error
- Solution The dist/ folder was not copied into the final image. Confirm the "COPY --from=builder /app/dist ./dist" line in the Dockerfile.
- Problem The application connects to a local database instead of the cloud one
- Solution Verify that DATABASE_URL is set in the Guara Cloud dashboard and that ConfigModule reads the variable correctly.
- Problem Docker build takes more than 10 minutes
- Solution Make sure node_modules and dist are in .dockerignore. The npm ci layer is reused between builds when package.json has not changed.
- Problem Health check fails but the application responds normally
- Solution The probe may hit before the app finishes starting. Increase initialDelaySeconds or simplify the health check to avoid external dependencies.
Performance tips for NestJS in containers
A few settings make a real difference when the application is under load:
-
Replica count. NestJS does not clusterize on its own. If you need multiple processes, scale horizontally with multiple replicas on Guara Cloud instead of running PM2 inside the container.
-
Lazy module loading. Modules loaded with
LazyModuleLoaderreduce cold start time in applications where many modules exist but not every request uses all of them. -
Fastify instead of Express. Switch the adapter to
@nestjs/platform-fastifyif the API receives more than a thousand requests per second. The throughput difference is measurable.
Does NestJS work with Docker without code changes?
Almost. You need to make sure main.ts listens on 0.0.0.0 and uses process.env.PORT. Everything else (Dockerfile, variables, build) is configuration outside the NestJS code.
How do I use PostgreSQL with NestJS on Guara Cloud?
Create a PostgreSQL instance from the Guara Cloud catalog, copy the connection URL, and set it as DATABASE_URL in the service environment variables. Use TypeORM or Prisma in NestJS to consume the connection.
Is billing in BRL?
Yes. Guara Cloud bills in BRL through Stripe. No dollar conversion, no surprises on your credit card.
Do I need Nginx in front of NestJS?
No. Guara Cloud provides HTTPS, a public domain, and routing without you configuring Nginx or TLS certificates manually.
Ship your NestJS API on Guara Cloud
Docker deploys, managed HTTPS, logs, and BRL billing. Infrastructure in São Paulo.