Docs menu
Customizing the build
How to shape the Go build on Primcloud: selecting packages, vendoring, build tags, native dependencies, and Debian packages via project.toml.
On this page
Most Go apps deploy on Primcloud with no custom build steps. The build finds every main package in your module, compiles it, and ships the binaries. When you need more, Go's own build directives cover the common cases: which packages to build, which build tags to apply, how to include native code, and how to install OS-level libraries.
There are no pre- or post-build hook scripts for Go. Anything that needs to run during the build has to fit into what go install does: build tags, go generate wired into your own tooling, or work another step in the build performs.
Selecting packages to build#
By default, every main package in the module is compiled, one binary per package. Two knobs shape that:
// +heroku install directive in go.mod. Name the exact packages to build:
module example.com/myapp
go 1.26
// +heroku install example.com/myapp/cmd/server example.com/myapp/cmd/worker
Only the listed packages are compiled. Everything else in the module is skipped, even if it has a main.
Standard Go build patterns work too. ./..., ./cmd/..., and other relative patterns are valid in the // +heroku install list; they're passed straight to go install.
Build tags#
The build always runs with -tags heroku. Files gated with //go:build heroku are included; files gated with //go:build !heroku are excluded. Use this to add or drop behavior specific to Primcloud, for example, a build-only file that registers a health-check handler:
//go:build heroku
package main
func init() {
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
}
You can't add tags beyond heroku; the tag list is fixed. If you need extra tags for a specific target, wire them into your own build tooling and commit the resulting files.
Vendoring#
Committing a vendor/ directory (generated with go mod vendor) tells the build to skip module downloads and compile against the vendored copies instead. Two reasons to do it:
- Reproducibility. The exact bytes of every dependency live in your repo. Builds don't depend on an upstream module proxy being available or serving the same content.
- Speed on cold builds. No network round-trips to download modules; compile starts immediately.
To vendor:
go mod vendor
git add vendor/
git commit -m "vendor dependencies"
vendor/modules.txt is the file the build checks for. go mod vendor generates it alongside the module source trees. Don't hand-edit it.
Native dependencies#
Two shapes of "native" your Go app can lean on:
cgo dependencies compiled from source. Packages that use cgo (import "C") need a C toolchain and any headers the C code references. The build environment has one: gcc, g++, make, and the C library headers are available while go install runs, so cgo is on by default, as it is on a Linux workstation with a compiler installed. Headers for a set of common libraries (OpenSSL, the PostgreSQL and MySQL clients, libxml2, zlib, and others) are there too.
The toolchain is build-only. The running app has no compiler, just your binaries and the base image's runtime libraries.
If a build fails on a cgo compile error, it's almost always a header the build environment doesn't have. You have two options:
- Install the required Debian packages. Add the missing
-devpackage throughproject.toml, covered below. - Turn cgo off. Set
CGO_ENABLED=0in the Secrets panel. If your dependencies don't strictly require cgo, they'll fall back to pure-Go implementations where those exist.
OS-level shared libraries. A cgo binary that dynamically links against a library such as libgeos needs it present at both build and runtime. When the library isn't in Primcloud's base image, install it through project.toml the same way.
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:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.deb-packages]
install = [
"libgeos-dev",
"libpq-dev",
]
Each entry is a Debian package name from the Ubuntu 24.04 archive, the base the run image is built on. Installed packages and their libraries are added to LD_LIBRARY_PATH for both build and runtime, so cgo compilation resolves headers and runtime dynamic linking resolves the shared objects, with no 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 withuri,suites,components,arch, andsigned_by(the source key as an ASCII-armored PGP block).
Note
Migrating from an Aptfile? An Aptfile at the repo root is not read. The build detects it, prints a migration warning, and installs nothing. Move your package list into project.toml under the schema above.