# Deploy a Node.js or Express API to your own VPS Deploy an Express, Fastify or other Node.js API to your own VPS: start script, lockfile install, migrations, PostgreSQL and Redis, from one short ox.toml. To deploy a Node.js API such as Express to a VPS with ox, give `package.json` a `start` script, listen on `PORT` and `HOST`, and deploy. ox detects the Node version, the lockfile install, your build script and the migrate tool, then runs the server as a systemd service behind Caddy, with no Docker or PM2. PostgreSQL and Redis come from the `[services]` table of the ox.toml below. ## What ox detects in a Node.js repo ox reads `package.json` and your lockfile. From the repo root it proposes: - The Node version from `engines.node` in `package.json`, or from `.nvmrc`, `.node-version` or `mise.toml`. With none of them, Node 24. - The install step from the lockfile: `npm ci` for `package-lock.json`, `pnpm install --frozen-lockfile`, `yarn install --frozen-lockfile` or `bun install --frozen-lockfile`. With no lockfile, `npm install`. - `npm run build` when there is a `build` script, such as a TypeScript compile, and `npm run start` when there is a `start` script (with your package manager in place of npm). - With no `start` script, `node `, but only when a web framework ox knows (Express, Fastify, Koa, Hapi, Restify, Polka or Hono's Node server) is a dependency. The file is the `main` field of `package.json` when that file is in the repo, or else the one `index.js`, `server.js` or `app.js` at the root. - The migrate step `npx drizzle-kit migrate` with a `drizzle.config.*`, `npx knex migrate:latest` with a `knexfile.*`, or `npx prisma migrate deploy` with `prisma/schema.prisma`, when the tool is a dependency. - When the repo has no ox.toml: PostgreSQL when `pg`, `postgres` or `@neondatabase/serverless` is a dependency, and Redis when `redis`, `ioredis` or `bullmq` is. A `start` script is the surest way: when ox cannot tell which file is the server, such as a `main` that points into a `dist` folder your build makes, it proposes nothing and `ox check` says why in a hint. Your app must listen on the port and address ox gives it in `PORT` and `HOST`. src/server.js: ```javascript import express from "express"; const app = express(); app.get("/health", (req, res) => res.send("ok")); app.listen(process.env.PORT, process.env.HOST); ``` package.json, the scripts ox uses: ```json { "type": "module", "engines": { "node": "24" }, "scripts": { "start": "node src/server.js", "migrate": "node src/migrate.js" } } ``` ## The ox.toml for an Express API ox.toml for an Express API with PostgreSQL and Redis: ```toml domains = ["api.example.com"] [app] start = "npm run start" health = "/health" [build] install = "npm ci" migrate = "npm run migrate" [services] postgres = {} redis = {} ``` - `domains`: the names your API answers on. ox gets the HTTPS certificate for each. - `start`: the command that runs your server. It is the same one ox detects, so you can leave it out. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install`: installs your packages from `package-lock.json`. - `migrate`: your own migration script, run on every deploy after ox takes a snapshot of the database. Leave it out if you have none. - `postgres` and `redis`: a database and a Redis for this app. Keep only the ones you use. ## PostgreSQL and Redis for a Node API ox gives your app `DATABASE_URL` for `postgres` and `REDIS_URL` for `redis` ([[services]](https://deploywithox.com/docs/config#services)). Both `pg` and `ioredis` take the URL as it is. Once a repo has an ox.toml, ox runs only the services it lists. src/db.js: ```javascript import pg from "pg"; import Redis from "ioredis"; export const db = new pg.Pool({ connectionString: process.env.DATABASE_URL }); export const redis = new Redis(process.env.REDIS_URL); ``` ## Variables and secrets ox also gives you `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `SESSION_SECRET`, on the dashboard or with `ox vars set SESSION_SECRET`, which asks for the value. Every key named in `.env.example` must be set before a deploy can start. ox does not set `NODE_ENV` for you, so set it to `production` yourself if your code checks it. ## Check and deploy the Node API Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, DATABASE_URL, REDIS_URL Set on the dashboard before the first deploy: SESSION_SECRET Ready to deploy. ``` ## If the Node deploy fails - `nothing to run: ox found no start command, static site, worker, or cron job; set [app] start (or declare [workers])`. `ox check` says this when `package.json` has no `start` script and ox could not pick the server file; the hint above it says why. Add a `start` script, or set `[app] start`. - `GET http://127.0.0.1:/health did not answer within 120s`. The app listens on a fixed port like 3000 instead of `process.env.PORT`, or `/health` returns a 404. Use the port ox gives you. - `set these before deploying: SESSION_SECRET`. A key from `.env.example` has no value yet. Set it, then deploy again. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set SESSION_SECRET and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily database backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/node