# How to deploy an Express / Node.js API on Ownkube

> Deploy an Express or plain Node.js API on Ownkube with npm start, a PORT env var, an optional managed Postgres, a health check, and autoscaling.

- **Published:** 2026-08-25
- **Author:** Ownkube team
- **Category:** How-To
- **Tags:** express, nodejs, deploy, tutorial, api
- **Canonical URL:** https://ownkube.io/blog/deploy-express-node-api
- **Cover:** https://ownkube.io/blog/deploy-express-node-api.png

---
An [Express](https://expressjs.com/) API needs almost nothing from a host beyond a process that runs `npm start`, a port it can bind to, and somewhere to send traffic once it is healthy. Ownkube handles all of that from a git push, with a managed Postgres available if the API needs one.

This walks through deploying a [Node.js](https://nodejs.org/en/docs) Express API on Ownkube Compute: the start command, the `PORT` environment variable, an optional database, a health check endpoint, and [horizontal autoscaling](/blog/autoscale-cpu-memory).

**TL;DR**

- Ownkube runs `npm start` (or your Dockerfile's `CMD`) and expects the app to bind to `process.env.PORT`.
- Add a managed Postgres if the API needs a database, and pass its connection string in as `DATABASE_URL`.
- Add a `/health` route so Ownkube's health checks can tell a live process from a stuck one.
- Turn on horizontal autoscaling so the API adds instances under load and idles back down when quiet.
- Metered billing suits most APIs: a mostly-idle service between requests barely registers on the CPU meter.

## 1. Confirm the app binds to the right port

Ownkube injects a `PORT` environment variable into the container. The app has to read it rather than hardcoding a port:

```js
// server.js
const express = require("express");
const app = express();

app.get("/health", (req, res) => {
  res.status(200).json({ status: "ok" });
});

app.get("/", (req, res) => {
  res.json({ message: "hello from Ownkube" });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`listening on ${port}`);
});
```

Make sure `package.json` has a `start` script that Ownkube can run directly:

```json
{
  "scripts": {
    "start": "node server.js"
  }
}
```

## 2. Add a Dockerfile (optional, recommended for control)

You can deploy straight from source, but a Dockerfile gives you a pinned Node version and a smaller runtime image:

```dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

FROM node:20-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production
EXPOSE 3000
CMD ["npm", "start"]
```

## 3. Push and create the app

Connect the repo from the dashboard, or use `okctl`:

```bash
okctl login
okctl apps create express-api --dockerfile Dockerfile
okctl deploy
```

Without a Dockerfile, Ownkube can build straight from `package.json` and run `npm start`, which is enough for a simple API with no native dependencies.

Ownkube allocates a `*.ownkube.app` hostname with automatic TLS. Attach a custom domain later from the app's settings if you need one.

## 4. Set environment variables

Set any config the API needs, including secrets, from the dashboard or the CLI. These are injected into the running container and never committed to the repo:

```bash
okctl env set NODE_ENV=production --app express-api
okctl env set API_KEY="your-secret-key" --app express-api
```

## 5. Add a managed Postgres, if the API needs one

Not every API needs a database, but if yours does, create a [Postgres box](/blog/add-managed-postgres-database) from **New > Database > Postgres** in the dashboard. It is a single-instance, private (in-region) box starting at $4 a month plus storage at $0.15 per GB, backups included. Multi-instance HA and public access are on the roadmap, not shipped yet.

```bash
okctl env set DATABASE_URL="postgres://user:password@db-host:5432/api_production" --app express-api
```

With something like `pg` or an ORM such as [Prisma](https://www.prisma.io/docs) or Drizzle, point the client straight at `process.env.DATABASE_URL`:

```js
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
```

## 6. Wire up the health check

Ownkube uses a health check endpoint to know when a new deploy is actually ready to take traffic, and to detect a process that is alive but stuck. Point it at the `/health` route from step 1:

```bash
okctl apps update express-api --health-check-path /health
```

A deploy only completes the rollout, and an old instance only gets terminated, once the new instance's health check passes. That is what makes rollouts zero-downtime instead of a race between old and new processes.

## 7. Turn on autoscaling

Most APIs see uneven traffic through the day. Horizontal autoscaling adds instances when CPU or memory crosses a threshold and scales back down when the API is quiet:

```bash
okctl apps update express-api --autoscale-min 1 --autoscale-max 5 --autoscale-cpu-target 70
```

Combined with metered billing, an API that is busy for an hour a day and idle the rest of the time pays close to nothing on CPU during the idle hours, since metered pricing bills both CPU and memory on what is actually used, not on the instance size.

```bash
okctl deploy --cpu-limit 500m --memory-limit 512Mi
```

## 8. Check logs and metrics

Logs and per-instance metrics are in the Ownkube dashboard as soon as the app is deployed, which is usually where a bad health check or a crash loop shows up first, before a user reports it.

## Moving to your own AWS later

If the API needs to run inside a specific AWS account, either your own or a customer's, for compliance or contractual reasons, the same Dockerfile moves over with no rewrite. Ownkube provisions the instance in that AWS account and keeps the same deploy flow, logs, metrics, and health checks attached. Railway does not offer that path since it only runs on Railway's own infrastructure. Our [Railway alternative comparison](/blog/railway-alternative-2026) goes through what changes and what does not when you make that move.

## FAQ

### Does Ownkube support WebSockets?

Yes. A standard Express app using `ws` or `socket.io` on the same HTTP server works without extra configuration, since Ownkube proxies the app's single port.

### How do I run database migrations for a Node API?

Set a release command, the same way you would run any one-off command before a deploy completes, for example `npx prisma migrate deploy` or `npx knex migrate:latest`:

```bash
okctl apps update express-api --release-command "npx prisma migrate deploy"
```

### Can I run a background worker alongside the API?

Yes. Deploy the worker as a separate Ownkube app that shares the same `DATABASE_URL`, and optionally a [Valkey cache box](/blog/add-valkey-redis-cache) (from $2 a month) if the worker needs a queue.

### What happens if I forget to set PORT?

You do not need to set `PORT` yourself. Ownkube injects it into the container, and the app should read `process.env.PORT` rather than hardcoding a value, as shown in step 1.

### Is there a free tier for small APIs?

No, Compute has no free tier. The Personal plan is $5 a month and loads $5 of wallet credit you then spend, so the plan price is also usable balance, and unused credit rolls over and never expires.

## Where Ownkube fits

An Express API mostly needs a place to run `npm start`, a port to bind, and a health check that tells the platform when it is actually ready, and Ownkube covers that without a cloud account or a YAML pipeline to write first. Add a managed Postgres if the API needs state, turn on autoscaling if traffic is uneven, and the rollout stays zero-downtime by construction. [Deploy your first app](https://app.ownkube.io/login).