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.node | Image |
|---|---|
| absent | node:22-slim |
>=22, ^22.1.0, 22.x | node:22-slim |
| below 18, or unparseable | node: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:
| Lockfile | Install | After the build |
|---|---|---|
pnpm-lock.yaml | pnpm install --frozen-lockfile | pnpm prune --prod |
yarn.lock | yarn install --frozen-lockfile | (none) |
package-lock.json | npm ci | npm prune --omit=dev |
| (none) | npm install | npm 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:
- An explicit entrypoint in the function's settings (argv form, never shell-joined).
scripts.start→npm start.- The
mainfield →node <main>. - 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=productionPORTset 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.