EthioDeploy
DocsPricingDomainsBlogChangelog
Deploy guides/NestJS

Deploy a NestJS app

NestJS apps build with nest build and need one setting: start the compiled dist/main instead of nest start. Add a Postgres addon, run migrations at start, and push.

Last updated October 29, 2026

What we run by default
Detected by
package.json
Install
npm ci / pnpm / yarn (from your lockfile)
Build
npm run build (nest build)
Start
npm start (nest start, which compiles again)

Override any of them in Settings → Build.

Fix this first: Set the Start command to node dist/main

We run npm start, and in a new Nest project that is nest start, which runs nest build before starting the app. It works, but every deploy and every wake from sleep pays for a full TypeScript compile. Set the Start command in Settings → Build to node dist/main so the container runs the output the build already made.

Prepare your app

The main.ts that nest new generates already reads PORT, and we set it for you. With no host argument, Node listens on every interface, so you don't need to change the call. A production-ready version:

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.set('trust proxy', 1);
  app.enableShutdownHooks();
  await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();

On the Fastify adapter, pass the host. Fastify listens only on localhost by default, and the health check can't reach it there:

await app.listen(process.env.PORT ?? 3000, '0.0.0.0');

The build runs npm run build, which is nest build and writes dist/. Dev dependencies are installed and kept, so @nestjs/cli can stay a dev dependency. Then set the Start command to node dist/main. If you keep a .ts file in the project root, Nest outputs dist/src/main.js instead, so start that path.

We build on Node 22 unless engines.node in package.json asks for another version. app.enableShutdownHooks() matters here: Nest's shutdown hooks are off by default, and the old container gets a SIGTERM when a new deploy goes live or the app goes to sleep.

Environment variables and @nestjs/config

Add variables in the project's settings. They're available both while the app builds and while it runs, so build-time values (like framework-public keys) work without extra setup. See Environment variables.

With @nestjs/config, load it once in the root module:

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}

Variables set in Settings take precedence over a .env file, so a local .env never overrides production values. Keep .env out of git anyway, and set ignoreEnvFile: true if you want production to read only the real environment.

Proxy and CORS

HTTPS ends at the platform's proxy, so your app sees plain HTTP. app.set('trust proxy', 1) in the main.ts above makes Express read the forwarded headers, so secure cookies are sent and @nestjs/throttler sees the real client IP.

If a frontend on another domain calls the API, enable CORS for that origin:

app.enableCors({ origin: process.env.FRONTEND_URL });

Health check

Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving.

The generated AppController answers GET / with 200, so a new project passes as is. Once you add a global prefix like app.setGlobalPrefix('api'), / returns 404. Add a health route, keep it out of the prefix, and set /health as the Health Check Path:

app.setGlobalPrefix('api', { exclude: ['health'] });

A plain @Get() that returns "ok" is enough. For a check that also pings the database, use @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').withTimeout(1000),
    ]);
  }
}

Terminus answers with an error status when a check fails, so a deploy whose database is unreachable never goes live.

Add a database

Add a Postgres addon and DATABASE_URL is injected automatically (a Redis addon injects REDIS_URL). The database is private to the project, so it isn't reachable while the app builds, only once the container runs. Run migrations from the Start command, not the Build command.

TypeORM

TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  autoLoadEntities: true,
  synchronize: false,
  migrations: ['dist/migrations/*.js'],
  migrationsRun: true,
})

Keep synchronize off in production: it changes the schema on every start and can drop data. migrationsRun: true applies pending migrations each time the app starts.

Prisma

Prisma 7 needs a driver adapter. For Postgres, pass DATABASE_URL to @prisma/adapter-pg:

import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  constructor() {
    super({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) });
  }

  async onModuleInit() {
    await this.$connect();
  }
}

The generated client isn't committed, so generate it before the build and apply migrations before the server starts. In Settings → Build:

Build command:  npx prisma generate && npm run build
Start command:  npx prisma migrate deploy && node dist/main

Deploy a NestJS app for free

The Free plan costs nothing and needs no card. It includes:

  • One project, live over HTTPS at <project-slug>.ethiodeploy.app.
  • 256 MB of memory and half a CPU core.
  • A managed Postgres, MySQL or Redis addon with 512 MB of storage. It doesn't expire.
  • A redeploy on every push, the same as the paid plans.

The catch is sleep. After 30 minutes without traffic the app stops, and the next request starts it again. We hold that request for up to 30 seconds while the app boots, so the first visitor after a quiet spell waits a little.

A compiled Nest API runs as one Node process and fits a small app in 256 MB. Start it with node dist/main, as above, so a wake from sleep runs your code straight away instead of compiling it first. Sleeping stops the process, so @nestjs/schedule cron jobs and in-memory caches stop with it. Keep cached data in the Redis addon and anything durable in Postgres.

When the app has to stay awake or needs your own domain, Pro is $10 or 1,399 ETB a month: always on, 1 GB of memory and a 5 GB database.

Deploy

  1. Sign in with GitHub and install the EthioDeploy app on the repo.
  2. Click “New Project” and pick the repo.
  3. Watch the build in the dashboard. When the health check passes, your app is live at <project-slug>.ethiodeploy.app. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.

Troubleshooting

Every deploy and wake starts with a TypeScript compile+

Cause: No Start command is set, so npm start runs nest start, which builds before running.

Fix: Set the Start command to node dist/main.

Cannot find module '/app/dist/main'+

Cause: A .ts file in the project root moved the entry point to dist/src/main.js.

Fix: Start node dist/src/main, or move the file out of the root.

Deploy fails: GET / returned 404+

Cause: A global prefix moved every route, or the root controller was removed.

Fix: Add a /health route excluded from the prefix and set it as the Health Check Path.

prisma migrate deploy fails in the build with a connection error+

Cause: The database addon is only reachable from the running container, not from the build.

Fix: Move it to the Start command: npx prisma migrate deploy && node dist/main.

Cannot find module '../generated/prisma/client'+

Cause: The Prisma client is generated, not committed, and nothing generated it.

Fix: Set the Build command to npx prisma generate && npm run build.

Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.

Questions

Yes. The Free plan runs one project with a 512 MB Postgres or Redis addon at no cost. The app sleeps after 30 minutes without traffic and wakes on the next request, and the container has 256 MB of memory. Pro ($10/month) keeps it always on with 1 GB.

No. We detect package.json, install from your lockfile and run npm run build. Set the Start command to node dist/main in Settings → Build so the app starts from the compiled output.

Yes. Fastify listens only on localhost by default, so call app.listen(process.env.PORT ?? 3000, '0.0.0.0').

Run them when the container starts. With Prisma, set the Start command to npx prisma migrate deploy && node dist/main. With TypeORM, set migrationsRun: true. The database isn't reachable during the build.

Ready to deploy?

Sign in with GitHub and pick your repo. No Dockerfile, no YAML.

More guides

GoTelegram botFirebase Studio

All guides on the deploy guides page. Custom domains, env vars, addons and billing are in the docs.

On this page
Prepare your appEnvironment variables and @nestjs/configProxy and CORSHealth checkAdd a databaseDeploy a NestJS app for freeDeployTroubleshootingQuestions
Deploying with an agent?Point it at llms.txt. Every guide is in one file there.Open llms-full.txt →
EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs NetlifyHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternatives
Resources
DocsBlogAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy