Noah

A Development Journal

This project is maintained by nzoschke

Runtime Profile Scripts

Recently, the Heroku Runtime introduced a change in how we run your code:

-  execlp("bash", "bash", "-c", command, (char *) NULL))
+  execlp("bash", "bash", "--login", "-c", command, (char *) NULL))

The introduction of a "login shell" brings us one step closer to the vision of the Heroku Runtime:

The Heroku Runtime operates a robust, secure, highly dynamic and massively scalable multi-tenant execution platform.

By running all dynos as a login shell, we improved the flexibility of the platform with a classic Unix primitive: profile scripts.

As the Bash Reference Manual explains, a login shell automatically sources /etc/profile then ~/.profile before executing the command. These scripts offer hooks to control the execution environment.

In a terminal, profile scripts customize the command prompt, append to $PATH for a package manager, etc.

On the platform, profile scripts influence the contract between the Heroku Runtime and your code:

Heroku Runtime sets $HOME and $PORT
Heroku Runtime sets values from `heroku config`
Heroku Runtime executes login shell 
Login shell sources /etc/profile, which sources $HOME/.profile.d/*
Login shell sources $HOME/.profile
Login shell executes command

Buildpacks

The most immediate use of profile scripts on Heroku is for buildpacks, which should now add .profile.d/ scripts:

$ heroku run "cat .profile.d/ruby.sh"
Running `cat .profile.d/ruby.sh` attached to terminal... up, run.1
export GEM_PATH=${GEM_PATH:-$HOME/vendor/bundle/ruby/1.9.1}
export LANG=${LANG:-en_US.UTF-8}
export PATH="$HOME/bin:$HOME/vendor/bundle/ruby/1.9.1/bin:$PATH"
export RACK_ENV=${RACK_ENV:-production}

This script fills in the environment that the Ruby needs for Bundler and Rails, but honors any settings in heroku config.

Previously, buildpacks altered heroku config to set the expected environment:

$ heroku config
GEM_PATH: vendor/bundle/ruby/1.9.1
LANG:     en_US.UTF-8
PATH:     bin:vendor/bundle/ruby/1.9.1/bin:/usr/local/bin:/usr/bin:/bin
RACK_ENV: production

Although this worked, it was a leaky abstraction. heroku config is intended for app config and secrets like DATABASE_URL, not static settings for a buildpack. Here, heroku config:remove PATH breaks the app.

Your App

A .profile script may also prove useful to your application.

For example, you can use one to set up the ~/.ssh/ directory in every dyno to use SSH private and public key contents from heroku config:

$ cat .profile
#!/bin/bash

echo "setting up ~/.ssh via .profile"
mkdir -p $HOME/.ssh

cat >$HOME/.ssh/config <<EOF
StrictHostKeyChecking no
EOF

[ -n "$SSH_PRIVATE_KEY" ] && echo "$SSH_PRIVATE_KEY" >$HOME/.ssh/id_rsa
[ -n "$SSH_PUBLIC_KEY" ]  && echo "$SSH_PUBLIC_KEY"  >$HOME/.ssh/id_rsa.pub

Now heroku run "git clone git@github.com:username/private-repo.git" works.

A dyno is always a container you fully control. With login shell and profile scripts you have an additional Unix-y way of setting up the environment.

Heroku believes that profile scripts are a common pattern for any application, both installed by a buildpack and used for custom application logic.

This feature should excite intrepid Unix fans. If this is you, the Heroku Runtime is hiring engineers to help run billions of shells.

-Noah