Xeonr Developer Docs
Runtimes

Node.js

Anything with a package.json. The package manager follows your lockfile and the start command follows your manifest.

A Node.js function is any source tree with a package.json at its root.

{
  "name": "hello",
  "engines": { "node": ">=22" },
  "scripts": { "start": "node server.js" }
}
// server.js — listen on $PORT. That is the only rule.
import { createServer } from "node:http";
createServer((_, res) => res.end("hello")).listen(process.env.PORT);

A malformed package.json fails the deploy with a precise error rather than a confusing build failure later.

Node version

The major version comes from engines.node. The range is read leniently — the first number in it wins — because the useful signal is which major you tested on.

engines.nodeImage
absentnode:22-slim
>=22, ^22.1.0, 22.xnode:22-slim
below 18, or unparseablenode:22-slim

Node 22 is the default, and versions below 18 are ignored rather than honoured — they are out of support upstream, so running them would be a slow-motion security problem.

Package manager

Selected by the lockfile you committed, in this order:

LockfileInstallAfter the build
pnpm-lock.yamlpnpm install --frozen-lockfilepnpm prune --prod
yarn.lockyarn install --frozen-lockfile(none)
package-lock.jsonnpm cinpm prune --omit=dev
(none)npm installnpm prune --omit=dev

pnpm and yarn are enabled through corepack. Yarn Berry has no equivalent of prune --prod, so dev dependencies stay in the image — that costs image size, not correctness.

A repository with several lockfiles is usually mid-migration; the first match wins deterministically, and deleting the stale one is how you force the issue.

With no lockfile the install is not reproducible — npm ci requires one, so npm install is used instead and the resolved dependency tree can differ between builds. Commit a lockfile.

Build script

If scripts.build exists it runs after the install and before the prune, so it has your dev dependencies available. This ordering is deliberate: installing with --omit=dev up front succeeds and then fails the build on a missing bundler.

Start command

The first of these that applies:

  1. An explicit entrypoint in the function's settings (argv form, never shell-joined).
  2. scripts.startnpm start.
  3. The main field → node <main>.
  4. A conventional file: index.js, server.js, app.js, index.mjs.

If none apply the deploy fails asking you to add a start script, a main field, or an explicit entrypoint.

The runtime image

  • Base: node:<major>-slim
  • Working directory: /app
  • NODE_ENV=production
  • PORT set to the configured port (8080 by default)
  • Any system packages you declared

Everything in your source tree ends up in the image, so a .env file or a credential committed to the repository ships with it. Use secrets instead.

On this page