Development and Configuration Principles
Last updated March 09, 2022
Using Heroku as your deployment environment not only impacts the runtime aspects of your application, but also its development process. Included here are several principles of application development and configuration central to the development experience on Heroku.
These development and configuration principles are one of several categories of best-practices when developing for Heroku. Please review all the principles of architecting applications to gain a complete understanding of properly developing apps on Heroku.
Applications & codebases
When developing an app most of the work is done on the application’s codebase which is stored in some form of version control system (VCS) like GitHub (recommended), GitLab, BitBucket or other. The VCS keeps track of the many revisions of the codebase and coordinates the modifications to the source code amongst development teams of all sizes.
On deployment to Heroku your app will be transmitted via the Git VCS. Though Git is a very capable VCS, it’s not necessary for you to be using it as your primary VCS to deploy to Heroku. You are free to choose the right tool for your development workflow and organizational constraints.
When developing an application it is often necessary to have more than one version of that application running at any one time. Consider running a development branch locally, a staging version with the latest features available to your stakeholders, and a production version with only the approved, stable, features for your end-users.
On Heroku, maintain multiple deployment environments, e.g., staging, QA, production, by creating a Heroku app for each environment. Simply deploy the codebase revision appropriate for each environment to the corresponding app on Heroku.
To make the association between the app and its stage clear, namespace each Heroku app according to environment, e.g.,
Alternatively, consider Heroku Pipelines.
If there are multiple codebases that form a functioning system, each component should be deployed as an independent app (each with their own environment-specific deploys), which, in concert, form a distributed system.
Complex systems that want to maintain a high development velocity should follow this pattern and create independent but cooperative apps that work together to deliver the necessary functionality. It is common to see a segregation of responsibilities based on end-user segment, e.g.,
myapp-api. This establishes clearly-defined areas of responsibility and integration points between the apps, which untangles the overall complexity often present in large, monolithic applications.
Application dependencies, such as plugins and required third-party libraries should be explicitly declared and isolated. There should be no reliance on packages already installed on the Heroku runtime. When dependencies are isolated in this manner instantiating new environments is trivial and there is no leakage of incompatible versions from the system.
All modern language toolchains support dependency declaration and isolation. Consider Ruby’s bundler and
bundle exec, Python’s Pip and Virtualenv, Clojure’s Leiningen etc… When you deploy an app to Heroku, these dependencies are fetched and stored alongside your app to create an isolated, compressed, executable version of the app called a slug.
Non language-specific dependencies, such as binary executables, should be vendored, or stored, with the application codebase.
An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc). This includes database and other backing service locations, third-party credentials like AWS or Twitter, and per-deploy settings such as the hostname or concurrency levels.
Such configuration should not be stored in the codebase. This exposes private resources in the version control system and requires unnecessary code modifications between environments.
On Heroku, application configuration is specified in environment variables using
heroku config. Local development follows the same pattern as well, storing config data in the local environment and using tools such as Heroku Local to assist with environment management.
Backing services, or an any service the app consumes over the network as part of its normal operation, are attached to the app via a URL or other locator/credentials stored in the config. For instance, data stores like MongoDB or PostgreSQL, message queues, and email services, are all specified as URLs in the app’s config.
Consuming external resources this way allows you to easily and quickly swap out providers or modify runtime services without changes to the code and is how provisioning occurs in the Add-on Marketplace.
Apps on Heroku should be designed for continuous deployment by keeping the time, personnel and tools gap between development and production as small as possible. Any divergence between the development of an application and its execution in production can cause tiny incompatibilities, causing code that worked in development or staging to fail in production.
For instance, avoid using different services between development and production, even when adapters theoretically abstract away any differences in services. Using SQLite locally and PostgreSQL in production; or local process memory for caching in development and Memcached in production, may seem innocuous at first but can be the cause of a variety of production issues.