Framework guide

How to deploy a Node.js or Express API

To deploy a Node.js or Express API on Codara Cloud, make it listen on process.env.PORT, give package.json a start script, push it to GitHub and create an app from the repository. Codara builds it with Railpack (or your Dockerfile), runs it as an always-on container on HTTPS, and redeploys on every push. Add a /health route and handle SIGTERM so releases roll over without dropped requests.

By Codara Solutions · Reviewed

What does the API need before you deploy?

Three things. First, listen on the port in the PORT environment variable: Codara sets it (8080 unless you choose another port for the app). Second, a start script in package.json; Railpack runs it, or falls back to the main field or an index.js at the root. Third, a cheap health route that returns 200 without touching the database.

Railpack picks npm, pnpm, Yarn or Bun from your lockfile, runs your build script if there is one (for example tsc), and sets NODE_ENV=production. Pin the Node version with engines.node in package.json.

server.js
import express from "express";

const app = express();
app.use(express.json());

app.get("/health", (req, res) => res.sendStatus(200));
app.get("/api/hello", (req, res) => res.json({ hello: "world" }));

const port = Number(process.env.PORT) || 8080;
const server = app.listen(port, () => console.log(`Listening on ${port}`));

// Codara stops old containers with SIGTERM during a release.
process.on("SIGTERM", () => server.close(() => process.exit(0)));
package.json
{
  "type": "module",
  "scripts": { "start": "node server.js" },
  "engines": { "node": "22.x" },
  "dependencies": { "express": "^5.1.0" }
}

How do you create the app?

Install the Codara GitHub App on the repository’s owner, create a project, then open Apps → New app in the console. Pick the project and environment, choose GitHub repository and the branch, and submit: the wizard creates the app and starts its first build. After that, every push to the branch deploys.

Set the health check path to /health in the app’s settings. Codara then waits for new containers to answer before it moves traffic to them, and a release that never becomes healthy is marked failed while the previous one keeps serving.

Terminal · the same from the CLI
codaracloud apps create api --client my-workspace --project shop \
  --repo acme/shop-api --branch main
codaracloud deploy api --client my-workspace --project shop --source

codaracloud logs api --follow

How do you connect Postgres and run migrations?

Create a Postgres database in the same environment and connect it to the app. Codara injects DATABASE_URL for queries and DATABASE_DIRECT_URL, which bypasses any pooler, for migrations, and restarts the app to pick them up.

Run migrations as the app’s pre-deploy command, for example npx prisma migrate deploy or npm run migrate. It runs once in the new image before any traffic moves; if it fails, the release is abandoned and the current version keeps serving.

db.js
import pg from "pg";

export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

Can the API stay private, or run next to a worker?

Yes. An API that only your other services call can be created as an internal-only service with no public address (codaracloud apps create … --private). Its internal URL is shown as CODARA_PRIVATE_URL under Provided by Codara, and other services in the same environment call it there.

A queue worker or any other long-running process is a separate service in the same environment, with its own start command, sharing the database connection. Work that runs on a schedule belongs in a Codara job with a cron expression.

When should you use your own Dockerfile?

When you need system packages Railpack does not install, a specific base image, or a build you already run elsewhere. A file named exactly Dockerfile at the root of the build context is used as written; Railpack is skipped.

Questions

Does Express need to bind to 0.0.0.0?
Express’s app.listen(port) listens on all interfaces by default, which is what Codara needs. Do not pass 127.0.0.1 or localhost as the host: the platform could not reach the app.
Does it support Bun, pnpm or Yarn?
Yes. Railpack chooses the package manager from the packageManager field or your lockfile: pnpm-lock.yaml, bun.lock, yarn.lock or package-lock.json. Commit the lockfile.
Can it hold WebSocket connections?
Yes. The API runs as an always-on container, not as a function with a time limit. During a release, old containers receive SIGTERM, so clients should reconnect.

Keep going