# Heroku buildpack for Go: how detection, version pinning and install directives decide your build

> Heroku's classic Go buildpack treats a go.mod file as the trigger for a Go app, then resolves the toolchain version and the set of packages to compile from comments inside that same file. It suits teams already deploying to Heroku; the same directives are easy to misread and hard to debug once a build starts failing.

**heroku/heroku-buildpack-go** — Heroku's classic buildpack for Go apps

- Repository: https://github.com/heroku/heroku-buildpack-go
- Website: https://devcenter.heroku.com/categories/go-support
- Stars: 791 · Forks: 500
- Language: Shell
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/heroku-heroku-buildpack-go

## A go.mod file is the only thing that marks a repository as a Go app

The detection rule here is narrow and absolute: the buildpack claims a repository when it contains a go.mod file. There is no language sniffing of source files, no keyword scan, and no fallback to a hand written build script. That single condition explains both the appeal and the sharp edge of the project. A fresh clone with one go.mod file at the root is enough to get a Go app detected and built with no configuration at all.

The build output names each stage, so you can tell from the deploy log which path the code took. In the example given by the documentation, a push to a Heroku remote prints the detection line, the toolchain install line, the compile command, and the process type discovery step:

```bash
$ git push heroku main
...
-----> Go app detected
-----> Installing go1.27.1... done
-----> Running: go install -v -tags heroku .
-----> Discovering process types
       Procfile declares types -> web
```

Two details in that log matter more than the rest. The compile invocation always carries the heroku build tag, so files guarded by a Heroku specific build constraint take part in the build. And process types come from the Procfile rather than from any convention the buildpack infers, which is what makes the next section about the default Procfile worth reading before you deploy.

## Three layers decide which Go toolchain compiles your code

Version selection is where most surprises live, because there are three inputs and they are consulted in a fixed order. The buildpack reads the standard go directive in go.mod. When that directive is absent it falls back to its own DefaultVersion. On top of both sits the environment variable GOVERSION, which wins over any file directive.

Inside go.mod, a build constraint style comment overrides the plain go directive:

```bash
// +heroku goVersion go1.27
```

The comment takes precedence over the go directive but loses to GOVERSION, so a team can change the toolchain for one environment without editing the repository. The syntax has one hard error case: a version below go1.11 fails the build outright because modules do not exist in older Go releases.

The environment variable is the coarsest and sharpest knob. Setting it to a major version resolves to the newest patch in that series, while a full patch version pins exactly:

```bash
heroku config:set GOVERSION=go1.27   # Will use go1.27.X, where X is the latest minor release in the 1.27 series
heroku config:set GOVERSION=go1.27.0 # Pins to go1.27.0
```

The comment in the documentation calls go1.27.0 equivalent to go1.27 without auto updating, because Go does not publish .0 patch releases. Treat GOVERSION as the place where an emergency pin belongs, since it survives changes to go.mod.

## The buildpack compiles every main package it finds unless you stop it

Under the default behaviour the buildpack searches the code base for main packages and compiles them, ignoring anything under vendor. That sounds convenient until you realise that a repository with two main packages produces two binaries and no obvious way to choose between them. The fix is a directive in go.mod:

```bash
// +heroku install ./cmd/... ./special
```

The value is a space separated list of package specifications, and the common choices named in the documentation are ./cmd/... for everything under the cmd directory and ./... for the current directory and its sub directories. Which one to use depends on the layout of the repository, and the directive is the only supported way to narrow the build, since the automatic search already treats all main packages as candidates.

The same file carries the directive that marks the application as Heroku specific, so configuration for the platform lives in a place Go tooling itself ignores. Anything else in go.mod is treated as free form comments, which means you cannot overload the file with unrelated settings without risking a collision with these two reserved forms.

## The generated Procfile breaks the cmd directory layout

When the base directory has no Procfile and the buildpack can identify the base package, it writes one itself with a single web process type. The documented example for a module named github.com/heroku/example is minimal:

```bash
$ cat Procfile
web: example
```

The binary name in that file is the base package name, which is exactly why the convention fails for the cmd/<executable name> layout that Go projects increasingly adopt. The base package there is a library directory, not a program, so the generated command does not name a binary that exists. The documentation states the remedy plainly: with a cmd directory structure you need to create a Procfile yourself.

The default is also limited to one process type. A service that needs a separate worker process will get a Procfile with only web declared, and process types come from that file, so declaring the worker is a manual step rather than something the buildpack infers from your package layout.

## A vendor directory changes what the compiler is allowed to see

Vendor handling is conditional on two things at once. If a top level vendor directory exists and the go.sum file has a size greater than zero, then go install runs with -mod=vendor. The effect is that dependency downloads and checksum verification are skipped, and only the dependencies present in that top level vendor directory are used.

This is a useful escape hatch for builds that cannot reach a module proxy, or for teams that want the build to be a pure function of the checked in tree. It is also a silent source of mismatch. A stale vendor directory produces a build that succeeds while linking against older library code than the go.mod file requests, and nothing in the build output warns you, because the difference is visible only by comparing the vendor contents with the module requirements.

Note the condition on go.sum as well. A repository that vendors dependencies but keeps an empty go.sum will not trigger vendor mode at all, so the build falls back to normal module resolution and network downloads.

## Compile hooks run before and after go install inside the build

The buildpack looks for two optional executables at fixed paths, bin/go-pre-compile and bin/go-post-compile. If the file exists and is executable, it runs at the corresponding point, before compilation or after it. The documented use is installing extra tools, for example a database migration binary:

```bash
#!/bin/bash
set -e

go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.1
```

Because compiled executables land in bin, a post compile hook can itself be written in Go when the install directive covers that package. The documentation gives the layout that makes this possible:

```bash
$ cat go.mod
// +heroku install ./cmd/...
$ ls -F cmd
client/ go-post-compile/ server/
```

The requirement that the hook be executable, plus the fixed path, means the hook has to be committed with the executable bit set and cannot be renamed. A hook that fails aborts the build, which is convenient for correctness but leaves no partial deploy to inspect.

## Private git sources depend on one config var naming scheme

For dependencies hosted in private git repositories, the buildpack installs a custom git credential handler. Any tool that shells out to git is expected to pick it up transparently, and the documentation notes it has only been tested with GitHub repositories over https using personal access tokens.

The handler scans the application's config vars for names shaped like GO_GIT_CRED__<PROTOCOL>__<HOSTNAME>, with periods in the hostname replaced by double underscores. A matching value is used as the username. If the value contains a colon, the text before the colon is the username and the text after it is the password; with no password present, x-oauth-basic is used as the password. The documented example sets a single var:

```bash
heroku config:set GO_GIT_CRED__HTTPS__GITHUB__COM=FakePersonalAccessTokenHere
```

The tradeoff is narrow reach. Only https access to GitHub style hosts is described, and the token lives in a config var rather than in a secrets manager, so rotation and audit procedures have to account for that.

## Local verification runs the build inside the Heroku stack image

The repository ships a Makefile and a shell test suite, and the buildpack is written in Shell, so the test harness runs in Docker rather than on a bare developer machine. Make and docker are both required. The full suite is one target:

```bash
make test
```

The Makefile defaults to the heroku-26 stack and to the test/fixtures/mod-basic-go126 fixture, and it maps the stack name to a build image tag. A single test function from test/run.sh can be selected by name:

```bash
make test TEST=testModBasic
```

Contributors are asked to fork the project, add test functions to test/run.sh, and add matching fixtures under test/fixtures/<fixture name>. The lint path runs shellcheck over tracked scripts and checks formatting with shfmt, and a parallel target runs every test function in its own container for isolation. For an engineer evaluating the project, the practical value here is that fixture based builds can be reproduced locally without touching a live Heroku app.

## Conclusion

Adopt this buildpack if your Go service already ships to Heroku and you want the platform to pick the toolchain version from go.mod instead of a hand written pipeline. Do not adopt it if you need multiple services from one repository, non Go assets outside the Procfile model, or private module sources other than git over https, since the README documents only that one credential path. Before the first deploy, check the go directive in go.mod, add an explicit install directive if the repository holds more than one main package, and set GOVERSION as a config var when you want to pin a patch release.

## FAQ

### How does the Heroku buildpack for Go decide that a repository is a Go application?

The buildpack detects a Go application when the repository contains a go.mod file. The detection is based on that file alone, and the deploy log reports it with a Go app detected line.

### How do I pin a specific Go version for a Heroku build?

Set the GOVERSION config var to the version you want, for example heroku config:set GOVERSION=go1.27.0 to pin exactly, while a major version such as go1.27 resolves to the newest patch release in that series. GOVERSION takes precedence over the go directive and the +heroku goVersion comment in go.mod.

### Why does my Heroku Go build produce more than one binary or the wrong process type?

By default the buildpack compiles every main package it finds in the code base and ignores any under vendor, so a repository with several main packages produces several binaries. Use an install directive such as // +heroku install ./cmd/... to limit the packages, and create a Procfile yourself when the base package is not itself a program.

## Sources

- [heroku/heroku-buildpack-go on GitHub](https://github.com/heroku/heroku-buildpack-go)
- [License: MIT](https://github.com/heroku/heroku-buildpack-go/blob/main/LICENSE)
- [Project website](https://devcenter.heroku.com/categories/go-support)
- [README](https://github.com/heroku/heroku-buildpack-go/blob/main/README.md)
- [Releases](https://github.com/heroku/heroku-buildpack-go/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/heroku-heroku-buildpack-go
