Open-source project
heroku/heroku-buildpack-php avatar
heroku/heroku-buildpack-php

heroku-buildpack-php: Composer, PHP and a web server on Heroku

Heroku's classic buildpack for PHP apps

815 stars1,543 forksShellMIT

At a glance

What is it?
The official Heroku buildpack for PHP detects a composer.json, resolves the platform packages a PHP application needs, and installs a runtime plus either Apache2 or Nginx on the dyno. It is MIT licensed and actively released, and the interesting part for most teams is the custom platform repository mechanism it exposes.
Who is it for?
Adopt this buildpack if you deploy PHP applications to Heroku and want the platform maintained rather than self configured, especially if you need custom PHP extension packages from your own Composer repository. Do not adopt it if you have already moved your PHP deployment to containers, since the buildpack exists to produce a Heroku dyno image.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The buildpack that decides your app is PHP

This is the official Heroku buildpack for PHP applications, and it does three things: it decides whether your application is a PHP application, it builds that application, and it installs a web server to serve it.

The detection step is the one that surprises people. You need at least an empty composer.json in your application, and the README gives the sequence:

bash
    $ echo '{}' > composer.json
    $ git add composer.json
    $ git commit -m "add composer.json for PHP app detection"

An empty JSON object is enough. The buildpack does not need a complete manifest to claim the application.

The conflict case is documented too. If you also have files from other frameworks or languages that could trigger another buildpack, for example a package.json which might cause your code to be detected as a Node.js application even if it is a PHP application, then you have to set the buildpack manually:

bash
    $ heroku buildpacks:set heroku/php

That uses the officially published version. To use the default branch from GitHub instead, the README gives the alternative:

bash
    $ heroku buildpacks:set https://github.com/heroku/heroku-buildpack-php

So there are three states a PHP application can be in: auto detected because composer.json exists, explicitly assigned to heroku/php, or explicitly assigned to the GitHub URL to track the repository's default branch. The last one is a development convenience that couples your production build to a moving branch.

Further usage instructions live on the Heroku Dev Center, under the PHP support category, and the homepage for the project points at the same place.

Composer is the dependency manager and the platform package source

The buildpack uses Composer for dependency management, supports all recent versions of PHP as runtimes, and offers a choice of Apache2 or Nginx web servers.

The interesting mechanism is that Composer does more than install your application's packages. The buildpack uses Composer repositories to resolve platform dependencies, and the platform packages are named things like php and ext-something. That is, the PHP interpreter itself and its extensions are delivered as Composer packages.

This design has consequences. A PHP extension that Heroku's default repository does not carry can be built and published by you into a Composer repository, and the buildpack will install it. The extension is not a compiled-in binary of the platform image, it is a package resolved at build time through the same tool that resolves your application's dependencies.

The custom repository mechanism is exposed through a single config var, HEROKU_PHP_PLATFORM_REPOSITORIES. To add a custom Composer repository with additional or different platform packages, you add the URL of its packages.json to that var:

bash
    $ heroku config:set HEROKU_PHP_PLATFORM_REPOSITORIES="https://<your-bucket-name>.s3.<your-bucket-region>.amazonaws.com/heroku-24-amd64/packages.json"

Multiple repositories are allowed, separated by a space character, in ascending order of precedence, meaning the last repository listed is handled first by Composer for package lookups.

There is a support boundary stated plainly. Heroku cannot provide support for issues related to custom platform repositories and packages. If you point the var at your own bucket, you own the failure modes, which is fair but worth stating before you rely on it.

Controlling which packages a repository may supply

The buildpack exposes Composer's repository priority controls, and this is the part that lets you use a custom repository without taking over every package.

Three query strings on the repository URL control the behaviour. composer-repository-canonical takes true or false and defaults to true, meaning Composer treats the repository as the authoritative source for the packages it provides. composer-repository-exclude takes a comma separated list of excluded package names. composer-repository-only takes a comma separated list of allowed package names.

The example in the documentation makes the model concrete. This config var allows only the packages ext-igbinary and ext-redis from customrepo.com, and all other packages are looked up in the default repository:

bash
    $ heroku config:set HEROKU_PHP_PLATFORM_REPOSITORIES="https://customrepo.com/packages.json?composer-repository-only=ext-igbinary,ext-redis"

That is the difference between replacing the platform repository and supplementing it. If your own repository is canonical and only allows two extensions, then any other platform package still comes from Heroku's default repository, and the buildpack's normal behaviour is preserved.

There is a related switch for the default repository itself. If the first entry in the list is a hyphen rather than a URL, the default platform repository is disabled entirely. The README says this can be useful when testing development repositories, or to forcefully prevent the use of any packages from the default platform repository.

The order of the entries matters in both cases, since the list is in ascending order of precedence and the last one wins. That makes the whole feature readable as a priority list: your most specific repository last, the default first, and a hyphen in front to remove the default entirely.

Instructions for building the custom platform packages themselves, and a repository to hold them, are in the README in the support/build directory of the repository, which is the part you need if you are building an extension that Heroku does not ship.

How a build actually runs, and the local test loop

The Makefile in the repository shows the build sequence in enough detail to be useful, and it also shows how to run a build locally without deploying.

The buildpack has three stages, which appear as the three scripts copied into the build directory. Detect runs the detection logic on the source directory, Compile runs the compilation, and Report writes the build report. The local run target in the Makefile executes exactly that sequence inside a container:

bash
	@docker run --rm -v $(PWD):/src:ro --tmpfs /app -e "HOME=/app" -e "STACK=$(STACK)" "$(STACK_IMAGE_TAG)" \
		bash -euo pipefail -O dotglob -c '\
			mkdir /tmp/buildpack /tmp/cache /tmp/env; \
			cp -r /src/{bin,conf,support,composer.json} /tmp/buildpack; \
			cp -r /src/$(FIXTURE) /tmp/build_1; \
			cd /tmp/buildpack; \
			unset $$(printenv | cut -d '=' -f 1 | grep -vE "^(HOME|LANG|PATH|STACK)$$"); \
			echo -en "\n~ Detect: " && ./bin/detect /tmp/build_1; \
			echo -e "\n~ Compile:" && { ./bin/compile /tmp/build_1 /tmp/cache /tmp/env || COMPILE_FAILED=1; }; \
			echo -e "\n~ Report:" && ./bin/report /tmp/build_1 /tmp/cache /tmp/env; \
			[[ "$${COMPILE_FAILED:-}" == "1" ]] && exit $(COMPILE_FAILURE_EXIT_CODE); \
			[[ -x /tmp/build_1/bin/compile ]] && { echo -e "\n~ Compile (Inline Buildpack):" && (source ./export && /tmp/build_1/bin/compile /tm

Three details in there are worth reading. The stack is a variable, STACK, which defaults to heroku-24 and is converted into the build image tag heroku/heroku:NN-build. The fixture is also a variable, FIXTURE, defaulting to test/fixtures/default, which is what makes it possible to run the same buildpack against different sample applications. And the exit code is overridable through COMPILE_FAILURE_EXIT_CODE, with a comment explaining that this allows testing that bin/report works for failing builds, which is the kind of detail that only appears in a buildpack that has had to debug its own failure reporting.

For iteration, the Makefile comment recommends running the target under watchexec with a stop timeout of zero, so the build re-runs whenever a file changes, and the same recipe can be run with a different fixture by overriding the FIXTURE variable:

code
# brew install watchexec
# watchexec -qrc --stop-timeout 0 -- make run
# watchexec -qrc --stop-timeout 0 -- make run FIXTURE=test/fixtures/composer/basic_lock_v2

The second line overrides the fixture, which is how you test a specific application shape without redeploying.

Where this buildpack stops being the right tool

The obvious boundary is that it targets Heroku dynos. This is a buildpack in the Heroku sense, where the output is a prepared application directory that Heroku runs in a container. If your deployment has moved to your own container images, a Kubernetes cluster, or a serverless platform with a native PHP runtime, none of the custom platform repository machinery applies, because there is no dyno to install a PHP extension into.

The second limit is that the platform package set is what it is. The buildpack resolves PHP and its extensions from Composer repositories, and the default repository carries the packages Heroku publishes. Any extension you need that is not there requires you to build and publish it yourself, which means a compiler, a matching PHP version, and a repository to host it. For a common extension this is unnecessary work, but for an in-house extension it is a deliberate and reasonable investment.

The third limit is the Heroku dependency at the platform level. The buildpack references Heroku stack names, the build image tags derived from them, and the Dev Center for support. Running the buildpack outside Heroku, which the Makefile run target technically enables since it uses a Docker image, still gives you Heroku's stack rather than yours, and the platform packages you resolve are built for that stack.

The alternatives differ in where the PHP runtime comes from. A Dockerfile with a php base image and a Composer install gives you the same dependency management with no platform package repository at all, and it removes the Heroku layer from the equation entirely, at the cost of owning the runtime image, the extension compilation and the web server configuration yourself. The classic Heroku approach, using the buildpack implicitly rather than pointing at heroku/php, is the least work if you are already deploying to Heroku.

One more practical constraint is worth naming. The buildpack is described in the repository description as the classic buildpack for PHP apps, and the README says it supports all recent versions of PHP as runtimes. Recent is the operative word. If you need an old PHP version for a legacy codebase, the platform package for it may no longer be published, and the fix is again your own repository rather than a configuration change.

Maintenance, licence and the shape of the repository

The project is busy. The last push was on 2026-10-22, and the release list shows v297 from 2026-09-25, v296 from 2026-09-24 and v295 from 2026-09-02, so the buildpack ships numbered releases several times a month. The repository is not archived.

The licence is MIT, with the LICENSE file at the repository root, and the primary language is Shell, which matches the nature of a buildpack: most of the work is in the bin, conf and support directories, and the composition of the dyno is shell scripts calling out to Composer and the platform tools.

The repository layout shows a mature project rather than a wrapper. There is a bin directory for the detect, compile and report scripts, a conf directory for the configuration, a support directory holding the build logic including the custom platform package instructions, a test directory with fixtures, and a buildpack.toml which is the manifest Heroku reads. There is a composer.json, which the buildpack itself uses, and a requirements.txt listing bob-builder at 0.0.20 or newer plus natsort. The Gemfile and Gemfile.lock with an rspec_parallel file point at a Ruby test harness, and hatchet.json and hatchet.lock are the Heroku test suite configuration, which is the tool Heroku uses to run a buildpack against real platform expectations.

There is also a SECURITY.md, a CODE_OF_CONDUCT.md and a CHANGELOG.md, so the project handles the governance side properly.

The practical takeaway for a team deploying PHP to Heroku: the buildpack does the ordinary work of resolving a PHP runtime and a web server for you, and the part that earns your attention is the custom platform repository mechanism, which is capable and which Heroku explicitly declines to support once you use it. If you need one custom extension, that is a reasonable trade. If you are maintaining a mirror of the whole platform repository, you are running a second platform, and you should know that before you start.

Editorial conclusion

Adopt this buildpack if you deploy PHP applications to Heroku and want the platform maintained rather than self configured, especially if you need custom PHP extension packages from your own Composer repository. Do not adopt it if you have already moved your PHP deployment to containers, since the buildpack exists to produce a Heroku dyno image. Verify first that your composer.json is detected as a PHP application rather than claimed by another buildpack, that the PHP version you need is available as a platform package, and that the extension packages you rely on are in a repository you control before you depend on HEROKU_PHP_PLATFORM_REPOSITORIES.

Frequently asked questions

How do I make Heroku detect my application as a PHP application?

Add at least an empty composer.json to the project. The README shows `echo '{}' > composer.json` followed by a git add and commit, and notes that you must also manually set the buildpack with `heroku buildpacks:set heroku/php` if other files such as a package.json would make another buildpack claim the application.

How do I install a PHP extension that Heroku does not publish?

Build the extension as a Composer platform package, publish it in a repository with a packages.json, and add that URL to the HEROKU_PHP_PLATFORM_REPOSITORIES config var. The README states that Heroku cannot provide support for issues related to custom platform repositories and packages.

What can I control with the custom platform repository URL?

Three Composer repository query strings: composer-repository-canonical, which defaults to true, composer-repository-exclude with a comma separated list of excluded package names, and composer-repository-only with a comma separated list of allowed package names.

How do I disable the default platform repository?

If the first entry in the HEROKU_PHP_PLATFORM_REPOSITORIES list is a hyphen instead of a URL, the default platform repository is disabled entirely. The README says this is useful for testing development repositories or to forcefully prevent use of any package from the default repository.

Can I run the Heroku PHP buildpack locally before deploying?

Yes. The Makefile has a run target that executes detect, compile and report inside the stack build image, with STACK and FIXTURE as overridable variables, and the comment suggests running it under watchexec for quick iteration.

What license is heroku-buildpack-php released under?

MIT, with the LICENSE file at the repository root. The primary language is Shell, and the project publishes numbered releases regularly, with v297 dated 2026-09-25.

Official sources

  1. heroku/heroku-buildpack-php on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/heroku-heroku-buildpack-php.svg)](https://hysenlabs.com/projects/heroku-heroku-buildpack-php)

Was this useful?