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.
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
startscript inpackage.jsonand its lockfile committed (package-lock.json,yarn.lockorpnpm-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 yourpackage.jsonhas abuildscript. - 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:
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 listens on localhost unless told otherwise, so host is required.
await app.listen({ port: Number(process.env.PORT) || 3000, host: "0.0.0.0" });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:
{
"engines": { "node": "22.x" }
}TypeScript
Compile in the build script and run the output in the start script:
{
"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
/healthwith 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=infoVariables 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:
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:
- Enter your domain, for example
www.example.com, and click Add domain. - 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. - 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.
Learning path
Deploy your first app
- 1
Deploy your app
- 2
- 3
- 4
Keep reading
How to Add a Database to Your App
Create a managed PostgreSQL or MariaDB database next to your app and have its connection details handed to your app automatically, with examples for Node.js, Python and Django.
How to Connect Your Domain to Your App (with Free HTTPS)
Put your app on your own domain in three DNS steps. Covers www versus root domains, what each DNS record does, Cloudflare, and what to check when a domain will not verify.
How to Deploy a React App
Take a React app built with Vite or Create React App from your GitHub repository to a live HTTPS address, with environment variables, your own domain and automatic deploys on every push.
Deployment Failed? How to Find and Fix the Cause
Every failed deployment tells you which stage broke and why. How to read it, and the fixes for the failures we see most: missing start commands, port mix-ups, health checks and memory.
Ready to deploy?
Start on the Free plan: your first month is on us, and every app runs two copies for high availability.