Docs menu

Customizing the build

How to shape the Bun build on Primcloud: build script hooks, pruning, tools that expect Node.js, and Debian packages via project.toml.

On this page

Most Bun apps deploy on Primcloud with no custom build steps. The build installs Bun, restores your lockfile, runs your build script if one exists, and that's it. When you need more, there are three hook scripts in package.json and Debian package installs for OS-level libraries.

Build hooks#

The build runs three package.json scripts, in this order:

  1. heroku-prebuild: always runs first if defined.
  2. heroku-build if defined, otherwise build. Only one of the two runs (heroku-build wins if both are present).
  3. heroku-postbuild: always runs last if defined.

Missing scripts are skipped silently. Each script is executed with bun run <name>. A script that exits non-zero fails the build.

Example: bundle with build, generate a manifest in heroku-postbuild:

package.json
{
  "scripts": {
    "build": "bun build ./src/server.ts --outdir ./dist --target bun",
    "heroku-postbuild": "bun scripts/generate-manifest.ts",
    "start": "bun dist/server.js"
  }
}

Common patterns:

  • Bundling and framework builds go in build.
  • One-off deploy tasks that need devDependencies (schema generation, sitemap builds) go in heroku-prebuild or heroku-postbuild. Both run before pruning, and devDependencies stay installed afterwards.

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#

There's no "extra install command" surface. Everything installs from your lockfile. To adjust what gets installed, edit package.json, run bun install locally, commit the lockfile, 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:

project.toml
[_]
schema-version = "0.2"

[com.primcloud.buildpacks.bun]
enabled = false

Skip pruning. By default, the build runs bun prune after your build scripts to remove the packages in node_modules that your lockfile doesn't list. devDependencies stay installed either way. Skip the prune:

project.toml
[_]
schema-version = "0.2"

[com.primcloud.buildpacks.bun]
actions.prune_dev_dependencies = false

Do this only when your app needs a package at runtime that a build script installs outside the lockfile. The correct fix is almost always to add it to package.json and commit the lockfile.

Pruning runs on Bun 1.4.0 and newer. On an older pinned Bun the step is skipped.

Tools that expect Node.js#

A Bun app has Bun and nothing else: there's no node binary. Most build tools run fine under Bun, including one whose executable starts with a node shebang, such as Vite: with no node installed, Bun runs it. To say so explicitly, run the script with Bun's --bun flag, which makes Bun stand in for node:

package.json
{
  "scripts": {
    "build": "bun run --bun vite build"
  }
}

Native dependencies#

OS-level shared libraries. Anything your code or a dependency links against at runtime (image libraries, database client libraries, and so on) is provided by the operating system, not by your packages. Declare what's missing 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:

project.toml
[_]
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. Installed packages and their libraries are added to LD_LIBRARY_PATH for both the build and runtime phases, 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 with uri, suites, components, arch, and signed_by (the source key as an ASCII-armored PGP block).