Pin the Node.js version for a deploy: put the major in engines.node.
Set the major version in engines.node in package.json, for example "24.x". Most platforms read it, but some prefer .nvmrc, .node-version or a NODE_VERSION variable, and they disagree on which wins. On Canner, engines.node and the project’s Node.js version setting decide. It offers Node 24 and 22, and ignores .nvmrc.
Facts checked
Which file each platform reads
The same repository can build on three different Node versions depending on where you push it. This is what each platform documents, highest priority first.
| Platform | Reads, in order of priority | Ignores or doesn’t document | Versions |
|---|---|---|---|
| Canner | engines.node when the project setting doesn’t satisfy it; otherwise the project’s Node.js version setting | .nvmrc, .node-version, a NODE_VERSION variable | 24, 22 (22 is the default) |
| Vercel † | engines.node in package.json, then the project setting | .nvmrc isn’t mentioned | 26.x (default), 24.x, 22.x; majors only |
| Netlify † | NODE_VERSION variable, .node-version or .nvmrc, then the UI setting, then the build image default | engines isn’t mentioned | Any version nvm can install |
| Render † | NODE_VERSION, .node-version, .nvmrc, then engines | — | Default 24.22.1 for services created since 2026-10-21 |
| Heroku † | engines.node only | .nvmrc and .node-version aren’t mentioned | Default 24.x |
| Railway (Railpack) † | RAILPACK_NODE_VERSION, devEngines.runtime, engines.node, .nvmrc, .node-version, mise.toml or .tool-versions | — | Defaults to the current LTS |
† Competitor details come from each vendor’s own published pages on the dates shown; prices and plans change, so check the vendor before you decide. Vercel (2026-10-29) · Netlify (2026-10-29) · Render (2026-10-29) · Heroku (2026-10-29) · Railway (Railpack) (2026-10-29)
What to put in the file
Pin a major: 24.x. A major gets security patches without surprising you. An exact version like 24.11.0 freezes you on old patches where the platform allows it, and many platforms only offer majors anyway. An open range like >=18 has no ceiling, so the day a platform adds a new major, your build may move to it. Heroku’s docs give the same advice.
Keep .nvmrc if your team uses nvm or fnm, but treat it as a local convenience and keep it equal to engines.node. When the two disagree, the platform reading one and your laptop reading the other is how “works locally” builds fail.
// package.json: the file deploy platforms read
{
"engines": { "node": "24.x" }
}
# .nvmrc: for nvm and fnm on your machine (Canner ignores it)
24How Canner picks the version
Canner reads engines.node from the package.json in your project’s root directory and compares it with the project’s Node.js version setting (Settings, default Node 22). It only compares majors. Each case prints its own line in the build log:
- No `engines.node`: the setting is used. The log shows
[canner] node version: 22. - The setting satisfies the range: the setting is kept.
>=18stays on Node 22 rather than jumping to 24, so a broad range never moves a working project to a new major. - The setting doesn’t satisfy it: Canner takes the newest major it offers that does, and says so:
[canner] package.json engines.node="^24" isn't satisfied by Node 22; building on Node 24. - Nothing satisfies it: the build stops before installing:
package.json engines.node="20.x" needs a Node version Canner doesn't offer (available: 24, 22). Widen or remove engines.node, or set the project's Node version and turn on "force".The same happens with26.x, Vercel’s current default. - The range can’t be read: Canner keeps the setting and notes
Couldn't read engines.node="…". A range with an upper bound written as<=is one example; write>=18 <23instead of>=18 <=22. - Minor versions are not compared.
>=22.17counts as satisfied by Node 22. If a dependency needs a recent minor, choose Node 24.
Ignore engines.node on purpose
Sometimes a dependency declares a range your app doesn’t really need, or you want to test a major before changing package.json. The API has a node_version_forced field on the project: with it on, Canner builds on the setting and logs engines.node="…" ignored — this project pins Node 22. There is no dashboard switch for it yet.
# Use the project's version and ignore engines.node
curl -X PATCH https://api.canner.ca/v1/projects/my-app \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"node_version": "24", "node_version_forced": true}'Why a build picks the wrong Node
- The platform reads a different file than you edited. Netlify and Render read
.nvmrc; Heroku documents onlyengines, and Canner doesn’t read.nvmrc. Check the table above. - A higher-priority source wins. On Render or Netlify, a
NODE_VERSIONvariable set long ago in the dashboard beats your new.nvmrc. - The file is in the wrong directory. In a monorepo, the platform reads the
package.jsonof the directory it builds. On Canner that is the project’s Root directory. - npm doesn’t enforce `engines`. Unless
engine-strictis set, npm treats the field as advisory and only warns. Whether it changes the Node version is up to the platform, not npm. - The major is end-of-life or not offered. Node 20 is end-of-life. A platform that dropped it either refuses the build or moves you up.
Questions
Does Canner read .nvmrc or .node-version?
No. Canner reads engines.node in package.json and the project’s Node.js version setting. Keep .nvmrc for local tools if you like, set to the same major.
Which Node.js versions does Canner offer?
Node 24 and Node 22. New projects default to 22. The settings list comes from what is installed on the build servers, so it changes when a major is added.
Why is my engines field ignored?
npm treats engines as advisory unless engine-strict is set, and some platforms (Netlify, per its docs) choose Node from other files. On Canner it is only ignored when the project’s version is forced.
Should I pin an exact Node version like 24.11.0?
Usually not. A major range such as 24.x keeps security patches coming. Pin an exact version only to reproduce a bug, and only on a platform that installs exact versions.
Related
- GuideSvelteKit “Cross-site POST form submissions are forbidden”
- GuideDjango DisallowedHost and CSRF 403 after deploy
- GuideMixed content behind a proxy: fixes by framework
- GuideCORS errors that only happen in production
- RubyRuby on Rails hosting in Canada
- PythonDjango hosting in Canada
Push a Node app and read which version it got.
Every build log names the Node version and why it was chosen. More guides are under hosting by framework.