Skip to content

Conversation

@dzuelke
Copy link
Contributor

@dzuelke dzuelke commented Aug 13, 2025

(scroll down to comments to see what the output looks like in practice 🙃)

Background

Our buildpacks currently set web as the default process type, and the Procfile buildpack sets that as the default, too, if present. The process type's command wrapper (e.g. /lifecycle/launcher/web) is set as the ENTRYPOINT for the image. This wrapper sets the env vars from all buildpacks and layers, and execs the process type's command. Any arguments provided to docker run after the image name are forwarded to this command as additional arguments.

As a result, one typically cannot docker run <image-name> bash -c "echo hello world" without specifying --entrypoint launcher.

A workaround employed by some buildpacks is to define the default process type's command as ["bash", "-c"], and then make the default argument(s) whatever the actual command is.

However, this does not allow for additional arguments, so the above example doesn't work either (only docker run <image-name> bash, without further arguments).

The issue with web as the default

Furthermore, having web be the default process type is not actually useful in practice, because a user must pass at least -e PORT=$PORT and -p $LOCALPORT:$PORT to the docker run command for the image to work:

user@localhost:~$ docker run --rm -e PORT=8080 -p 8080:8080 image-name

Just launching the default process using docker run image-name may cause a successful startup, including a message of having bound to a port (many servers will default to e.g. 8080 if no $PORT is in the env), but it will not be reachable, and without some easy to access documentation, users may not quickly determine what they are doing wrong.

Solution

This proof of concept disables the default process flag for web, and sets a new process type, usage, as the default instead. This usage program shows a help screen with information on how to invoke the image, a list of process types, and usage information for individual process types.

A simple invocation of the image shows usage details and a list of process types:

user@localhost:~$ docker run --rm image-name

The help sub-command gives details on how to use a particular process type:

user@localhost:~$ docker run --rm image-name help web

In addition, unless the first argument is help or --help, the usage program execs all remaining arguments, meaning it's possible to launch arbitrary commands out of the box:

user@localhost:~$ docker run --rm image-name bash -c "echo 'hello world'"
user@localhost:~$ docker run --rm image-name uname -a
user@localhost:~$ docker run --rm image-name -- help # in case 'help' is a binary on $PATH (the 'bash' help command is just a builtin)

Future

This proof of concept is intended to showcase the soundness of this approach, and allow others to test the usability of the idea. For the final implementation, the following should probably be changed:

  • move implementation to its own buildpack
  • make that buildpack last in the list for all groups in the heroku builder (so that it overrides default setting of web by the Procfile buildpack)
  • skip execution when building on Heroku (so that no usage process shows up in the heroku ps process table)
    • fairly easy via e.g. $DYNO
  • probably have that buildpack emit some special default notes about -e and -p for the web process case
    • could just be a hard-coded default for web
  • decide whether or not we want the "per-process-type help file" feature (could easily be added later if we deem relatively generic info on process types enough for now, except maybe the above -e/-p stuff for web)
  • possibly convert the "entrypoint" usage.sh program from Bash to Rust
    • especially if we want to keep the "per-process-type help file" feature, which right now relies on quick-and-dirty grepping of /layers/config/metadata.toml
  • maybe allow a process type name as the first argument, as if ENTRYPOINT was launcher, allowing docker run --rm image-name web etc
    • easy to add later, hard to remove once it's in ;)

Notes

  1. this implementation currently only works on projects without a Procfile (since the Procfile buildpack runs last and re-sets web to be the default)
  2. Heroku looks for a web process type to boot an app for HTTP traffic, not the default process type, so no impact there 💜
  3. Once added to the builder, a dedicated buildpack would immediately provide basic usage info to all existing buildpacks! 🎉

GUS-W-19330133

Our buildpacks currently set `web` as the default process type, and the Procfile buildpack sets that as the default, too, if present. The process type's command wrapper (e.g. `/lifecycle/launcher/web`) is set as the `ENTRYPOINT` for the image. This wrapper sets the env vars from all buildpacks and layers, and execs the process type's command. Any arguments provided to docker run after the image name are forwarded to this command as additional arguments.

As a result, one typically cannot `docker run <image-name> bash -c "echo hello world"` without specifying `--entrypoint launcher`.

A workaround employed by some buildpacks is to define the default process type's command as `["bash", "-c"]`, and then make the default argument(s) whatever the actual command is.

However, this does not allow for additional arguments, so the above example doesn't work either (only `docker run <image-name> bash`, without further arguments).

Furthermore, having `web` be the default process type is not actually useful in practice, because a user must pass at least `-e PORT=$PORT` and `-p $LOCALPORT:$PORT` to the `docker run` command for the image to work:

    $ docker run --rm -e PORT=8080 -p 8080:8080 image-name

This proof of concept disables the default process flag for `web`, and sets a new process type, `usage`, as the default instead. This `usage` program shows a help screen with information on how to invoke the image, a list of process types, and usage information for individual process types.

A simple invocation of the image shows usage details and a list of process types:

    $ docker run --rm image-name

The `help` sub-command gives details on how to use a particular process type:

    $ docker run --rm image-name help web

In addition, unless the first argument is `help` or `--help`, the `usage` program `exec`s all remaining arguments, meaning it's possible to launch arbitrary commands out of the box:

    $ docker run --rm image-name bash -c "echo 'hello world'"
    $ docker run --rm image-name uname -a
    $ docker run --rm image-name -- help # in case 'help' is a binary on $PATH (the 'bash' help command is just a builtin)

This proof of concept is intended to showcase the soundness of this approach, and allow others to test the usability of the idea. For the final implementation, the following should probably be changed:

- move implementation to its own buildpack
- make that buildpack last in the list for all groups in the heroku builder (so that it overrides default setting of `web` by the Procfile buildpack)
- probably have that buildpack emit some special default notes about `-e` and `-p` for the `web` process case
- skip execution when building on Heroku (so that no `usage` process shows up in the process table)
- possibly convert the entrypoint `usage.sh` program from Bash to Rust

1. this implementation currently only works on projects without a `Procfile` (since the Procfile buildpack runs last and sets `web` as default)
2. Heroku looks for a `web` process type to boot an app for HTTP traffic, not the default process type, so no impact there
3. Once added to the builder, a dedicated buildpack would immediately provide basic usage info to all existing buildpacks!

GUS-W-19330133
The idea here is to allow buildpacks to provide some more info for specific process types by placing a file (named after the process type) into a particular layer (whose name is known to the usage program, which can then find the usage info text file).

In a standalone implementation of the buildpack, the default text for the `web` process could also be different, and remind users to supply `-e PORT` and `-p` options to `docker run`.
@dzuelke dzuelke requested a review from a team as a code owner August 13, 2025 19:51
@dzuelke dzuelke marked this pull request as draft August 13, 2025 19:51
@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 13, 2025

The default "welcome" screen:

dzuelke@localhost:~$ docker run --rm php-cnb-hello-world         

██╗    ██╗███████╗██╗      ██████╗ ██████╗ ██╗   ██╗███████╗
██║    ██║██╔════╝██║     ██╔════╝██╔═══██╗███╗ ███║██╔════╝
██║ █╗ ██║█████╗  ██║     ██║     ██║   ██║█╔████╔█║█████╗  
██║███╗██║██╔══╝  ██║     ██║     ██║   ██║█║╚██╔╝█║██╔══╝  
╚███╔███╔╝███████╗███████╗╚██████╗╚██████╔╝█║ ╚═╝ █║███████╗
 ╚══╝╚══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ═╝     ╚╝╚══════╝

This help screen is the default process type for your CNB app image.

It can provide general instructions, list available process types, show help
for specific process types, and execute arbitrary commands.

Invoking this Usage Help
========================

Running your image without any arguments, or with `help' or `--help',
will display this screen:

    $ docker run --rm <this-image>
    $ docker run --rm <this-image> help
    $ docker run --rm <this-image> --help

Basic Usage Summary
===================

    $ docker run --rm <image-name> (help | --help) [process-type]
    $ docker run --rm --entrypoint <process-type> <this-image> [<argument>...]
    $ docker run --rm [-it] <this-image> [--] <command> [<argument>...]

Available Process Types
=======================

The following process types are available in this image:

  - web

Getting Help for a Process Type
===============================

To show help for a process type, pass its name after `help', like so:

    $ docker run --rm <this-image> help <process-type>

Launching a Process Type
========================

To launch a specific process type, specify it as the `--entrypoint', e.g.:

    $ docker run --rm --entrypoint <process-type> <this-image>

Some process types may require certain environment variables to be set, or
ports to be forwarded from the container, in order to be usable. Refer to
the help output for the respective process type for further information.

For example, a `web' process type typically requires a forwarded port, and
the environment variable `$PORT' specifying the in-container port number:

    $ docker run --rm --entrypoint web -p 8080:8080 -e PORT=8080 <this-image>

Executing commands
==================

When no entrypoint is specified (or with `--entrypoint usage'), and when
not providing `help' or `--help' as the first argument after the image name,
the given arguments will be executed as regular commands.

To launch an interactive shell, use the `-it' option, and specify `bash' as
the command:

    $ docker run --rm -it <this-image> bash

You may pass arbitrary additional arguments to commands, for example:

    $ docker run --rm -it <this-image> bash --login

To completely bypass this help tool, specify `--entrypoint launcher', e.g.:

    $ docker run --rm -it --entrypoint launcher <this-image> uname -a

Further reading
===============

For additional documentation on how to run buildpacks-built images, refer to
the documentation at Buildpacks.io:
  https://buildpacks.io/docs/for-app-developers/how-to/build-outputs/specify-launch-process/

@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 13, 2025

It shows a shorter introduction if explicitly called with help or --help:

dzuelke@localhost:~$ docker run --rm php-cnb-hello-world --help

██╗   ██╗ ███████╗ █████╗   █████╗ ███████╗
██║   ██║ ██╔════╝██╔══██╗ ██╔═══╝ ██╔════╝
██║   ██║ ███████╗███████║ ██║ ███╗█████╗  
██║   ██║ ╚════██║██╔══██║ ██║  ██║██╔══╝  
╚██████╔╝ ███████║██║  ██║ ╚█████╔╝███████╗
 ╚═════╝  ╚══════╝╚═╝  ╚═╝  ╚════╝ ╚══════╝

Basic Usage Summary
===================

    $ docker run --rm <image-name> (help | --help) [process-type]
    $ docker run --rm --entrypoint <process-type> <this-image> [<argument>...]
    $ docker run --rm [-it] <this-image> [--] <command> [<argument>...]

Available Process Types
=======================

The following process types are available in this image:

  - web

@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 13, 2025

Calling help <processtype> shows instructions for that process type's invocation:

dzuelke@localhost:~$ docker run --rm php-cnb-hello-world help web
       
██╗   ██╗ ███████╗ █████╗   █████╗ ███████╗       ██╗    ██╗███████╗██████╗ 
██║   ██║ ██╔════╝██╔══██╗ ██╔═══╝ ██╔════╝       ██║    ██║██╔════╝██╔══██╗
██║   ██║ ███████╗███████║ ██║ ███╗█████╗   ██╗   ██║ █╗ ██║█████╗  ██████╔╝
██║   ██║ ╚════██║██╔══██║ ██║  ██║██╔══╝   ╚═╝   ██║███╗██║██╔══╝  ██╔══██╗
╚██████╔╝ ███████║██║  ██║ ╚█████╔╝███████╗ ██╗   ╚███╔███╔╝███████╗██████╔╝
 ╚═════╝  ╚══════╝╚═╝  ╚═╝  ╚════╝ ╚══════╝ ╚═╝    ╚══╝╚══╝ ╚══════╝╚═════╝ 

This process type starts PHP-FPM and the Apache HTTPD web server and serves
the application on the port specified in environment variable `PORT'.

Launching this Process Type
===========================

This process type requires a forwarded port to serve traffic on, and the
environment variable `PORT' specifying the in-container port number:

    $ docker run --rm --entrypoint web -p 8080:8080 -e PORT=8080 <this-image>

The command above maps port 8080 on the host to port 8080 in the container.

Additional Usage Information
============================

The launched program, `heroku-php-apache2', supports various options and
arguments for customizing PHP-FPM or HTTPD, or e.g. specifying a document root.

Complete usage information is available by passing argument `--help' to
the `web' entrypoint:

    $ docker run --rm --entrypoint web <this-image> --help

@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 13, 2025

Arbitrary commands can be executed against the image:

dzuelke@localhost:~$ docker run --rm php-cnb-hello-world bash -c 'echo "hello world"'
hello world
dzuelke@localhost:~$ docker run --rm php-cnb-hello-world uname -a                    
Linux f1b32a12d4a8 6.10.14-linuxkit #1 SMP Sat May 17 08:28:57 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
dzuelke@localhost:~$ docker run --rm -ti php-cnb-hello-world bash    
heroku@f41b68441f8d:/workspace$ echo "hi"; exit
hi
exit

@edmorley
Copy link
Member

move implementation to its own buildpack

I wonder if heroku/procfile might be the best place for this, rather than a separate buildpack? Since:

  1. Until we have system buildpack support, any utils buildpacks need to be explicitly listed in project.toml any time an app customises their buildpacks list (which includes any app that uses more than one language). And whilst users might remember to add heroku/procfile they probably won't know to add others.
  2. By having all process related functionality in one buildpack it prevents confusing interactions between the two (eg procfile being added after the usage buildpack)
  3. It's one less buildpack/repository for us to maintain (Dependabot, releases, fixing CI, ...)

@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 15, 2025

Yeah, possibly, @edmorley.

@dzuelke
Copy link
Contributor Author

dzuelke commented Aug 18, 2025

Related discussion: heroku/buildpacks#15

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants