TutorialApp Hosting

How to Deploy a Node.js App (Express, Fastify or NestJS)

A Node.js API or web server is one of the easiest things to deploy on Chajio Cloud: if it starts with npm start on your computer, it will almost certainly start here. This guide covers the two lines of code every Node.js app needs to get right, TypeScript builds, choosing a Node.js version, and going live on your own domain.

CChajio Cloud team September 25, 2026 9 min read
On this page

Before you start

  • A Chajio Cloud App Hosting plan. The Free plan is enough to follow along. Once you order, you get an email with your sign-in details, and the first time you sign in you choose your own password.
  • Your code on GitHub, in a repository you own or can access. A public repository you don't own works too.
  • A Node.js app with a start script in package.json and its lockfile committed (package-lock.json, yarn.lock or pnpm-lock.yaml). Express, Fastify, NestJS, Koa and Hono all work the same way.

Step 1: Get your app ready

Chajio Cloud builds a Node.js app in three steps:

  • Install your dependencies from your lockfile, including development dependencies, so build tools such as TypeScript are available.
  • Build with npm run build, if your package.json has a build script.
  • Start with npm start.

Listen on PORT, on every interface

This is the part that matters most. Your server must listen on the port in the PORT environment variable, and on all network interfaces, 0.0.0.0, rather than only localhost. Get either wrong and your app starts but can’t be reached. Here is how in each framework:

Express
const express = require("express");
const app = express();

app.get("/", (req, res) => res.json({ message: "Hello from Chajio Cloud" }));
app.get("/health", (req, res) => res.json({ ok: true }));

const port = process.env.PORT || 3000;
app.listen(port, "0.0.0.0", () => console.log(`Listening on ${port}`));
Fastify
// Fastify listens on localhost unless told otherwise, so host is required.
await app.listen({ port: Number(process.env.PORT) || 3000, host: "0.0.0.0" });
NestJS (src/main.ts)
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000, "0.0.0.0");

Choose your Node.js version

Without a version, your app builds on an older default. Pick one with a .nvmrc file containing just the major version, or with engines in package.json:

package.json
{
  "engines": { "node": "22.x" }
}

TypeScript

Compile in the build script and run the output in the start script:

package.json
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "tsx": "^4.19.0",
    "@types/node": "^22.0.0"
  }
}

Keep typescript in devDependencies as usual; it’s installed for the build. If your start script runs ts-node or tsx directly, switch to compiled output: it starts faster and uses less memory.

Shut down cleanly

When you deploy, the old version of your app is told to stop with a SIGTERM signal once the new one is ready. Finishing the requests already in flight before exiting means no visitor sees an error during an update:

const server = app.listen(port, "0.0.0.0");

process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
});

Step 2: Create a project

A project is the home for one app, along with its database and domain. In your dashboard, open Projects and click New project:

Name
Lowercase letters, numbers and hyphens, up to 24 characters. It becomes part of your app’s free web address and can’t be changed later, so pick something short, like shop or api.
Plan
The plan the project draws its resources from.
Resources
CPU, memory and storage for this project, pre-filled with what your plan has left. The defaults suit most apps.

Click Create project. Setting it up takes about a minute, and you land on the deploy guide straight away, which walks you through three short steps: Source, Configure and Deploy. You can pick your repository while the project finishes setting up.

Step 3: Connect your repository

On the Source step, choose where your code comes from:

Your repository
Click Connect GitHub and approve access, which lets Chajio Cloud read your code and hear about your pushes. Then choose the repository and the branch to deploy, usually main. From now on, every push to that branch deploys automatically.
Public repository
Paste the URL of any public GitHub repository, no GitHub sign-in needed. Since you don’t own it, pushes can’t trigger deploys: deploy by hand, or tick Auto-deploy new releases to follow the project’s published releases.

If your app lives in a subfolder of the repository, as in a monorepo, set Build context to that folder, for example apps/web. Otherwise leave it as ., the repository root. Then click Connect repository and Continue.

Step 4: Configure

The Configure step has two runtime settings, both changeable later without a rebuild, and your environment variables.

Container port
Leave it at 3000. Your app reads PORT, so it listens on whatever you set here. If your app ignores PORT and always uses, say, 8080, set 8080 here instead.
Health check path
Use /health with a route like the one in the Express example. An API often has nothing at /, and a 404 there would count as unhealthy.

Add the variables your app reads with process.env, for example:

JWT_SECRET=a-long-random-string
CORS_ORIGIN=https://www.example.com
LOG_LEVEL=info

Variables live on the project's Environment tab. Add each one as a name and a value, then click Save & restart: your app restarts with the new values in moments, no rebuild needed.

Your build receives the variables too. A Dockerfile gets the ones it declares with an ARG line, and an auto-detected build gets all of them. A variable that went into your current build is marked Build. Change one of those and the dashboard offers to Redeploy, because only a new build picks up the new value.

PORT is set by the platform from the container port, so you can't set it yourself. If you attach a database, its connection variable appears here too, locked, because the platform manages it for you.

No .env file needed

Libraries like dotenv are for your computer. On Chajio Cloud the variables are already in process.env when your app starts, so keep .env out of Git and don’t rely on it in production.

Click Save and continue.

Step 5: Deploy

The Deploy step shows a summary of the repository, branch and build context. Click Deploy now and the build log starts streaming: your code is fetched, dependencies are installed and your app is built and started. The deployment moves through Queued, Building and Deploying to Success.

The first build takes a few minutes. Later builds reuse the parts that didn't change, so they are usually quicker. When it finishes, your app is live on a free live.chajio.cloud address with HTTPS already switched on. You'll find the address on the project's Overview.

If the deployment fails, the dashboard names the stage that broke and why, and the build log shows the details. Our troubleshooting guide covers the usual causes.

Adding a database

On plans that include one, create a managed PostgreSQL or MariaDB database on the project’s Databases tab and give it the variable name DATABASE_URL. Your app then receives the connection string automatically:

db.js
const { Pool } = require("pg");

const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5 });

module.exports = pool;

With Prisma, run prisma migrate deploy before your server starts, for example "start": "prisma migrate deploy && node dist/index.js". More in How to Add a Database to Your App.

Connect your domain

Your app already works on its free address. To serve it from your own domain, open the project's Domains tab:

  1. Enter your domain, for example www.example.com, and click Add domain.
  2. The dashboard shows two DNS records to add where your domain's DNS is managed. A TXT record proves the domain is yours. A CNAME record routes visitors to your app, or an A or ALIAS record for a root domain like example.com. Copy the values exactly as shown.
  3. Click Verify. Once both records check out, a free HTTPS certificate is issued automatically, usually in under a minute, and your app is live on your domain.

DNS changes can take a while to spread, so if Verify doesn't find a record yet, give it a few minutes; the dashboard also re-checks on its own. The full walkthrough, including root domains and Cloudflare, is in How to connect your domain to your app.

If a frontend on another domain calls your API, add that frontend’s address to your CORS settings, for example with the cors package: app.use(cors({ origin: process.env.CORS_ORIGIN })).

After launch

  • Every push deploys. Push to your branch and a new deployment starts on its own. Your current version keeps serving visitors until the new one passes its health check, so updates don't cause downtime. You can also deploy by hand from the Deployments tab.
  • Roll back in seconds. If a release misbehaves, open Deployments and click Rollback to this version on an earlier successful deployment. Nothing is rebuilt; the previous version simply comes back.
  • Watch it run. The Logs tab streams your app's output live, with secrets hidden. The Overview shows your app's status and address, with Redeploy, Restart and Stop buttons.

Your app runs as two copies

Every plan runs two copies of your app, so a crash or an update never takes it offline. That has one consequence worth designing for: anything an app keeps only in its own memory or on its own disk isn't shared with the other copy, and it's gone after a redeploy.

Keep sessions in your database or a cache service (with express-session, use a store such as connect-pg-simple) and store uploads in object storage. In-memory caches and rate limiters work, but each copy keeps its own.

Using Socket.IO? Connect with transports: ["websocket"] on the client, since its fallback mode expects every request to reach the same copy. To send a message to users connected to either copy, add a Socket.IO adapter backed by your database.

Common problems

The deployment fails because the app “did not become healthy”

Check the Logs tab. The usual causes are listening on localhost (Fastify’s default), a hard-coded port that differs from the container port, a health check path your app doesn’t have, and a crash on start because a required variable isn’t set.

npm ci fails because package.json and package-lock.json are out of sync

Run npm install on your computer, commit the updated lockfile and push. Builds install exactly what your lockfile says, which is what keeps them reproducible.

“Missing script: start”

Add a start script to package.json, such as "start": "node index.js".

Cannot find module ‘dist/index.js’

The build didn’t produce the file your start script runs. Check the build script exists and that outDir in tsconfig.json matches the path in start.

For anything else, Deployment Failed? How to Find and Fix the Cause explains how to read a failed deployment.

Keep reading

Ready to deploy?

Start on the Free plan: your first month is on us, and every app runs two copies for high availability.

View App Hosting plans