Docs menu

Customizing the build

How to shape the Ruby build on Primcloud: rake tasks, custom install steps, native gem dependencies, and Debian packages via project.toml.

On this page

Most Ruby apps deploy on Primcloud with no custom build steps. The build runs bundle install, then rake assets:precompile if the task exists, and that's it. When you need more, you have three extension points: your own rake tasks, BUNDLE_* environment variables, and Debian package installs for OS-level libraries.

Build hooks#

The Ruby build does not run arbitrary shell scripts. Custom build-time work goes through rake tasks that hook into or supplement the tasks the build already invokes.

Enhance assets:precompile. Add extra work to the asset pipeline by defining a rake task with enhance:

lib/tasks/postbuild.rake
namespace :assets do
  task :postbuild do
    # Generate a build stamp, warm a cache, etc.
    File.write("public/BUILD_SHA", ENV.fetch("BUILD_SHA", "unknown"))
  end
end

Rake::Task["assets:precompile"].enhance do
  Rake::Task["assets:postbuild"].invoke
end

Rake runs the enhancement after assets:precompile completes. Because rake assets:precompile runs automatically on every Rails deploy, no extra config is needed to trigger it.

Non-Rails apps. If your app isn't Rails and doesn't already run rake at build time, you can add a Rakefile at the root, include rake in your Gemfile.lock, and define whatever tasks you need, but the build won't invoke them unless they're chained into assets:precompile. For non-Rails builds where you need a hook, the cleaner path is to move the work into your Procfile command's startup (release-time work) or into an on_worker_boot callback if you're on Puma.

Custom install steps#

To adjust bundle install's behavior, set Bundler configuration via BUNDLE_* environment variables in the Secrets panel. Common changes:

  • BUNDLE_WITHOUT: override the default development:test exclusion. BUNDLE_WITHOUT=development:test:doc skips docs too.
  • BUNDLE_JOBS: increase to parallelize gem downloads. BUNDLE_JOBS=4 is a reasonable ceiling.
  • BUNDLE_RETRY: number of times to retry a failed gem download. Defaults to Bundler's own default.

The rest of Bundler's config surface is available via the same BUNDLE_* pattern; see Bundler's config reference.

What you cannot change: BUNDLE_DEPLOYMENT and BUNDLE_FROZEN are set by the build. Setting them yourself in the environment has no effect on the build.

Environment available to rake tasks. All environment variables set in the Secrets panel are present when rake runs. Read them with ENV.fetch("KEY") in your Rakefile.

Native dependencies#

Two categories of "native" your Ruby app can rely on:

Native gems with bundled binaries. Gems like nokogiri, bcrypt, sqlite3, pg, and sassc build C extensions at install time or (increasingly) ship precompiled binaries per Ruby version and platform. These install as part of bundle install. Native gems are compiled against the OS libraries in Primcloud's build image (Ubuntu 24.04 or 26.04, depending on the builder), so precompiled binaries for x86_64-linux / aarch64-linux land automatically when available.

OS-level shared libraries your code (or a native gem) links against: libxml2, libpq, libmariadb, libvips, image processing libraries, and so on. These are provided by the operating system, not by Bundler. Declare them in project.toml, covered below.

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",
  "libpq-dev",
  "poppler-utils",
]

Each entry is a Debian package name from the Ubuntu 24.04 archive (or 26.04, matching the builder). Packages install into their own layer and their libraries are added to LD_LIBRARY_PATH for both the build and runtime phases, so require "pg" and other native gem loads resolve against them without extra config, and CLI tools they ship (pdftotext from poppler-utils, vips from libvips, etc.) end up on PATH.

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).

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.