Docs menu

Customizing the build

How to shape the Python build on Primcloud: release-phase commands, custom install steps, native dependencies, and Debian packages via project.toml.

On this page

Most Python apps deploy on Primcloud with no custom build steps. The build installs Python, resolves dependencies with the package manager it picked, and (for Django) runs collectstatic. When you need more, the extension points are release-phase commands in the Procfile, native-dependency handling that piggybacks on your package manager, and Debian package installs for OS-level libraries.

Build hooks#

The Python build does not read bin/pre_compile / bin/post_compile scripts. Those are a Heroku Classic convention that Primcloud doesn't carry forward. Two places to hang work off a deploy instead:

Release phase. Anything that has to run once per deploy against the built image (database migrations, cache warming, one-off data fixes) goes in the release process type in your Procfile. Primcloud runs it after the build succeeds and before the new pods take traffic. Rollback happens automatically if the release command exits non-zero.

Procfile
web: gunicorn myapp.wsgi
release: python manage.py migrate --noinput

Django's collectstatic. Runs automatically at the end of the build when Django is installed and manage.py is at the root, with no configuration needed. See Python on Primcloud → Release phase conventions.

Build backend hooks. If you need code to run during dependency install itself, hang it off your package's own build backend. Any package installed from a source distribution runs its pyproject.toml-declared build backend (setuptools, hatchling, poetry-core, uv-build, and so on), and those backends run whatever build steps they're told to. This fires during pip install, poetry sync, or uv sync when the package is installed from source rather than a pre-built wheel.

Custom install steps#

You don't configure the install command directly. The build picks one command per package manager and runs it. To shape what gets installed:

  • pip. Everything is driven by requirements.txt. Pin exact versions, include --extra-index-url lines, use environment variable interpolation for tokens, add -e path/to/local-package for editable installs of packages in your repo.
  • Poetry. pyproject.toml and poetry.lock drive it. Dependency groups let you keep dev/test tooling out of the runtime image: the build only installs --only main, so anything in [tool.poetry.group.dev.dependencies] (or any other non-main group) is skipped automatically.
  • uv. pyproject.toml and uv.lock drive it. Similarly, [dependency-groups.dev] and other non-default groups are skipped: the build runs with --no-default-groups, so only the main dependency table is installed.

The build does not read .buildpacks or buildpack.yml files.

Native dependencies#

There are two shapes of "native" your Python app can rely on:

Compiled Python packages. Packages that ship as pre-built wheels (numpy, pandas, cryptography, pydantic-core, and most of the popular ones) install without needing to compile anything. The appropriate wheel for the base image's Python version and architecture is downloaded and unpacked. This is the common case.

Packages that build from source. When a package only ships an sdist (or a wheel isn't available for the target Python version / architecture), pip / Poetry / uv compile it from source during the install. The build's base image includes a C toolchain and headers for the Python interpreter, so most C-extension packages build without extra setup.

Packages that depend on OS-level shared libraries (libpq for psycopg2-binary's sibling psycopg2, libxml2/libxslt for lxml built from source, libmagic for python-magic, and so on) need those libraries installed at both build and runtime. Declare them in project.toml, covered in the next section.

Tip

Binary wheels. Where a package publishes both a source distribution and pre-built wheels (like psycopg2-binary versus psycopg2, or mysqlclient versus mysql-connector-python), the binary-wheel variant almost always avoids the OS-library-and-toolchain requirement. Prefer it unless you have a specific reason not to.

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 = [
  "libpq-dev",
  "libxml2",
  "libxslt1.1",
  "libmagic1",
]

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 imports at runtime (and any C extensions being built during install) resolve their shared libraries without extra config.

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.