Docs menu
Customizing the build
How to shape the Node.js build on Primcloud: build script hooks, disabling steps via project.toml, native modules, and Debian packages.
On this page
Most Node.js apps deploy on Primcloud with no custom build steps. The build installs the runtime, restores your lockfile, runs your build script if one exists, and that's it. When you need more, there are three hook scripts, a few config knobs in project.toml, and Debian package installs for OS-level libraries.
Build hooks#
The build runs three package.json scripts, in this order, during the build phase:
heroku-prebuild: always runs first if defined.heroku-buildif defined, otherwisebuild. Only one of the two runs (heroku-buildwins if both are present).heroku-postbuild: always runs last if defined.
Missing scripts are skipped silently. Each script is executed with the installed package manager (npm run <name>, pnpm run <name>, or yarn run <name> to match).
Example: transpile with build, generate assets in heroku-postbuild:
{
"scripts": {
"build": "tsc && vite build",
"heroku-postbuild": "node scripts/generate-manifest.mjs"
}
}
Common patterns:
- Framework builds (
next build,nuxi build,nest build) go inbuild. Next.js and the others are covered on the framework leaf pages. - Client-side asset compilation (Vite, esbuild, webpack) also goes in
build. - One-off deploy tasks that need
devDependencies(schema generation, sitemap builds) go inheroku-prebuildorheroku-postbuild. Both run before pruning, which removesdevDependencieswith pnpm and Yarn and leaves them installed with npm.
Use heroku-build over build only when your build script must exist for local dev but should not run on Primcloud (rare). In that case, define heroku-build for the deploy behavior and leave build for local use.
Custom install steps#
The build doesn't expose an "extra install command" surface. Everything installs from your lockfile. To adjust what gets installed, edit package.json and the lockfile locally, commit, and redeploy.
Two related knobs sit in project.toml:
Disable build scripts entirely. If your app doesn't need any of the three hooks and you want to skip them for a marginally faster build:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.nodejs]
enabled = false
Skip dev-dependency pruning. By default, the build prunes node_modules after your build scripts. With pnpm and Yarn that removes devDependencies from the runtime image. With npm, the build runs npm prune: packages your lockfile doesn't list are removed and devDependencies stay installed. Skip that:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.nodejs]
actions.prune_dev_dependencies = false
With pnpm or Yarn, do this only when a runtime dependency is misclassified as a devDependency and you can't move it. The correct fix is almost always to move it into dependencies. With npm, devDependencies stay installed either way, so skip pruning only when your app needs a package at runtime that a build script installs outside the lockfile.
Native dependencies#
Two shapes of "native" your Node.js app can rely on:
npm packages with native addons. Packages like sharp, bcrypt, sqlite3, node-canvas, and anything that uses node-gyp ship platform-specific native code that either downloads a prebuilt binary (via node-pre-gyp / prebuild-install) or compiles from source during install. The build has the C/C++ toolchain available at build time, so compilation-from-source works out of the box: the install command runs as it would locally on Linux.
The compiled artifacts are cached inside node_modules, so a repeat deploy with the same lockfile doesn't recompile. Bumping the Node.js major forces a recompile because the ABI changes.
OS-level shared libraries. Anything your code (or a native addon) links against at runtime: libvips for sharp, libpq for a native Postgres client, ImageMagick, libwebp, libxml2, and so on. These are provided by the operating system, not by npm. Declare them in project.toml, covered in the next section.
Debian packages#
When your app needs OS-level libraries or CLI tools that aren't in Primcloud's base image, declare them in a project.toml at the repo root. Primcloud's build installs them automatically when it finds the [com.heroku.buildpacks.deb-packages] table.
Declare packages to install:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.deb-packages]
install = [
"libvips-dev",
"imagemagick",
"ffmpeg",
]
Each entry is a Debian package name from the Ubuntu 24.04 archive. Packages are installed into their own layer and their libraries added to LD_LIBRARY_PATH for both the build and runtime phases, so require('sharp') at runtime finds libvips without extra config, and CLI binaries like ffmpeg are on PATH for build scripts and workers.
Advanced options (rarely needed):
- Skip transitive dependencies:
{ name = "some-package", skip_dependencies = true } - Force reinstall:
{ name = "some-package", force = true } - Install from a URL:
download = ["https://example.com/package-1.2.3.deb"] - Add a custom Debian repository: a
[[com.heroku.buildpacks.deb-packages.sources]]table withuri,suites,components,arch, andsigned_by(the source key as an ASCII-armored PGP block).
Note
If you're migrating from Heroku Classic and have an Aptfile at the repo root, the build does not read it. It detects the file and prints a migration warning, then exits without installing anything. Move your package list into project.toml under the schema above.