The 12 Factor App is a fantastic little document that outlines best practices for creating highly maintainable, scalable web services. In my opinion, it’s sadly not known as well enough as it should be, as mentioning it often results in blank stares.
To help combat this, I decided to create this “abridged” version that anyone should be able to read through quickly. Please keep in mind that many concepts have been drastically simplified and for the best experience, you should really read the original material linked above.
Without further ado, here is an abridged version of the 12 Factor App.
Track your code using some kind of version control, like Git or Mercurial. If you’re building a distributed system or using service-oriented architecture (micro-services too), then each component or service should get its own repository in the selected version control strategy.
Make sure that all of your needed dependencies are clearly marked in some kind of “manifest”. This way anyone pulling down your code can easily get up and running with as few steps as possible.
Set your sensitive information (like authentication credentials) or other “non-internal” configurations using environment (or system) variables. A bonus side effect of this is that it makes it possible to configure your application for just about any environment without being locked down to say… “local”, “development”, “staging” and “production”.
Local Development with
For local development purposes, you can make use of
.env files which are loaded by your application on start up. There are many different variations of the
dotenv library for whatever language you’re using.
IV. Backing Services
Databases, Message Queues, Mail Servers, Third-Party APIs, etc… these are examples of “backing services”. I don’t feel a whole lot more needs to be said.
V. Build, Release, Run
Once the application can be run, it needs to be placed on the server where it can be configured as that instance requires. This is referred to as the “release stage”. Finally, we have the “run stage”. Simple enough, we boot up our deployed application instance and it does its job.
With web services, “state” should be avoided and application instances shouldn’t share anything outside of a backing service (like a database). This is because state can make it difficult to scale up as an application instance should be able to fail and immediately have another instance take its place with no problems.
VII. Port Binding
Your application should be a self-contained runtime (aside from backing services). This means it should also include your HTTP server container. Many languages, like NodeJS, don’t require a separate container as the Node eco-system provides this for you. However, there are others that do, like PHP which typically runs via Apache. Bottomline, starting your application should spin up exposed services and bind them to a port (or ports). Bonus points if you make this port optionally configurable via environment variables.
Don’t treat your application as a giant “uber-app”. Take advantage of your operation system’s process manager and split your application’s workload into multiple processes. For example, run your HTTP server using a “web” process and run background tasks using a “worker” process.
The benefit here is that it becomes much easier to scale up your application, as you can scale up the individual processes as needed.
Since your processes are running “stateless” code, they should be considered disposable. This means you should be able to start or stop a process at a moment’s notice. Ideally, your application’s processes should take as little time to start up as possible in order to reduce downtime when scaling up or recovering from failure (crashes).
Additionally, a process should “clean up” when shutting down gracefully. HTTP servers should release their port and quit listening, background workers should return jobs to the queue (or other backing service), etc. However, processes should also be resilient enough to survive “sudden death” or a non-graceful shutdown.
A great option is to adopt “Crash-Only Design” or always assume the worst case scenario. This can help you in your selection of technologies, like using message queues that offer an automatic “return to queue” function for messages that are associated with a disconnected client or have “timed out”.
X. Development / Production Parity
Keep your environments as close as possible! Deploy often and reduce the “ceremony” involved with deployments of your application. Implement a continuous deployment pipeline and have it push your code from version control to the servers whenever a commit or PR is checked in.
Additionally, take advantage of tools like Vagrant and Docker to get environments running locally that match what you’ll be using on your servers to reduce the risk of unknown and unwanted surprises during deployment.
Logs are your best friend in maintaining your application, and can be an indispensable tool for debugging issues. Treat your logs as simple text streams and send them out via
stdout. Then let external tools/services on the environment, not the application, capture these logs and send them to the appropriate destination.
XII. Admin Processes
Run your administrative commands, like
rake db:migrate, using an identical codebase and configuration as your target release. Also make sure to use a separate, isolated process when running these commands on deployment servers.