The 12factor manifest is a set of guidelines to help us build SaaS applications that can easily be operated and scaled without much effort. It was originally put together by the folks at Heroku and is as relevant today as it was when it was published in 2011.

Heroku uses the container model on their cloud. When you are pushing your code to Heroku, they package it up (following your Procfile description and the buildpacks you configured) and deploy your application for you. It’s no surprise that the manifesto applies so well to containers. In this article, we are going to go over briefly on each factor and see how they apply to the container model using Docker.

Table of contents

  1. Codebase
  2. Dependencies
  3. Config
  4. Backing Services
  5. Build, release, run
  6. Processes
  7. Port binding
  8. Processes
  9. Disposability
  10. Dev/Prod-parity
  11. Logs
  12. Admin Processes

1. Codebase

One codebase tracked in revision control, many deploys (source)

docker containers
Source: 12factor.net

Usually, you have a single application per project. That application should have its own Dockerfile and maybe a more optimized version for production, a prod.Dockerfile, or whatever convention you like.

The images are the deployment artifact. So, your CI/CD flow should end with a new version for the application image pushed to a registry.

2. Dependencies

Explicitly declare and isolate dependencies (source)

Docker solves this factor really well. In the Dockerfile, you describe every single dependency your application needs at the Operating System (OS) level. Besides that, you should also use your dependency manager for your language (NPM, Composer, Bundler, etc).

One thing to mention is that you should have different steps for each application, the reason is that you want to keep your layers small (more and smaller layers). Your dependencies have different update paces. Your OS dependencies usually don’t change that often, followed by your application dependencies (packages) which also don’t change that often compared to your source code, which changes many times a day.

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-02-dependencies-dockerfile

Take this example, for instance. You first install the OS dependencies (just as an example), then you add your dependency manager files and install the app dependencies. Only then you copy your source code to the image.

Let’s say you change your “index.js” file:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-02-dependencies-diff

Rebuilding it, this is the output:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-02-dependencies-sh

See that most layers say “Using cache” until step 6/8? That’s what we are talking about. You should frequently update all your dependency, just not many times a day. But make sure you have a routine that updates your OS and package dependencies every week, roughly, and make sure to do so every time a bugfix is released on any of your dependencies.

As we saw on the rebuilding output, when we only have application code changes for a new version of our application, only the steps after “COPY . /app” will be rebuilt; the rest should reuse the existing layers. If you add a new dependency, chances are you had to change source code anyways, so everything below “COPY package.json …” will be rebuilt. Same for OS-level dependencies, usually they are added to satisfy some app dependency, which also needs some source code. You got it.

By doing this, we increase the release speed because the servers will pull really small layers most of the time. The same applies to Composer or Bundler dependencies.

3. Config

Store config in the environment (source)

In the context of Docker containers, this one is all about injecting configuration via environment variables, secrets, ConfigMaps, or whatever other way your container orchestrator allows. You can pass these via command-line arguments when you’re running the image:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-03-docker-run-sh

It can also be described in your docker-compose.yml file:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-03-docker-compose-yml

Depending on the container orchestrator you use (Kubernetes, Swarm, Nomads, etc), they might provide solutions for you to keep configs/secrets separate from your applications. In Kubernetes, for instance, you can create secrets and map those secrets as environment variables in your PODs:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-03-k8s-secrets-yml

Then you can point to the secret in your Deployment manifest:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-03-k8s-deploys-yml

See the envFrom pointing to a secretRef that uses the name of the secret in question? That will inject the secrets as environment variables in that container.

Other more robust solutions include using Consul or Vault from Hashicorp, but I have only begun to experiment with those and I don’t feel “ready” to use it in a production environment yet.

One more thing I’d like to mention is that we used to only have environment variables being injected in the containers, but the truth is that we can have any kind of file as well. If you want to keep your secret as a PHP file that gets mounted in the container and your application uses it, it’s also possible.

4. Backing Services

Treat backing services as attached resources (source)

This is an interesting one. Since our containers are supposed to be stateless, we need something to keep some state in order to make anything useful with our applications.

Be it a database, object storage, or queue system, I would recommend running these in dedicated services outside of your cluster and pass in credentials and configs (host, ports, etc.) via configuration (see the factor above about configs).

You might have services that depend on other services that also run in your cluster. Kubernetes provides you a way to find the services running in the cluster (service discovery). You can use the name of the Kubernetes Service as the hostname, for instance. But your application must receive that as configuration as well, even though it might not change that often. This keeps your application easily configurable.

docker containers backing services
Source: 12factor.net

5. Build, release, run

Strictly separate build and run stages (source)

These steps are very explicit in the container world:

  • Build: docker build …
  • Release: docker push …
  • Run: kubectl apply -f …
docker burn build
Source: 12factor.net

6. Processes

Execute the app as one or more stateless processes (source)

Your container should be self-contained (no pun intended). Ideally, it should have a single process as PID 1. In reality, though, you might need more processes. Take PHP as an example. It needs both Nginx and PHP-FPM to handle HTTP requests. Kubernetes gives one solution to this problem: their smallest unit isn’t a container; it’s a POD. A POD consists of at least 1 container. In theory, you could have your PHP-FPM container and another container for Nginx, and they could talk to each other via TCP sockets (over localhost, since they are running on the same POD). The PHP-FPM container has one master process and some child processes, but PHP is already share-nothing, so it doesn’t violate the factor.

I prefer to have a single container for the application, so I usually pack PHP-FPM and Nginx in the same container, and they talk via Unix Sockets. This is much easier to scale horizontally, and you can use a process supervisor like the S6 Overlay which will handle signal forwarding. So when your container receives a SIGTERM or SIGKILL, the S6 overlay should forward those to your processes and they can gracefully terminate the app before the container is removed.

Another aspect of stateless processes is that you should not rely on memory or filesystem to store things. Remember, containers are ephemeral which means that they should be disposable. Sure you can use the file system of containers to download a file and do some processing on it, for example. But your application shouldn’t expect that some file will always be around (except for config files mounted via Kubernetes secrets).

It’s preferable that your assets (CSS, JS, etc.) are compiled in the build steps of your images, never at runtime. Runtime is for receiving requests and sending responses back. Also, you should prefer using a CDN for your assets. You can easily hook your webpack builds to deploy to a CDN and have your assets much closer to your end-users, making the general feel of your application faster.

If you need a session to keep the context of your users around between requests, prefer using a backing service for that (see factor IV. Backing services). Many frameworks allow you to use different storage drivers for your sessions, such as Redis, Memcached, or even the database.

Execute the app as one or more stateless processes (source)

Your container should be self-contained (no pun intended). Ideally, it should have a single process as PID 1. In reality, though, you might need more processes. Take PHP as an example. It needs both Nginx and PHP-FPM to handle HTTP requests. Kubernetes gives one solution to this problem: their smallest unit isn’t a container; it’s a POD. A POD consists of at least 1 container. In theory, you could have your PHP-FPM container and another container for Nginx, and they could talk to each other via TCP sockets (over localhost, since they are running on the same POD). The PHP-FPM container has one master process and some child processes, but PHP is already share-nothing, so it doesn’t violate the factor.

I prefer to have a single container for the application, so I usually pack PHP-FPM and Nginx in the same container, and they talk via Unix Sockets. This is much easier to scale horizontally, and you can use a process supervisor like the S6 Overlay which will handle signal forwarding. So when your container receives a SIGTERM or SIGKILL, the S6 overlay should forward those to your processes and they can gracefully terminate the app before the container is removed.

Another aspect of stateless processes is that you should not rely on memory or filesystem to store things. Remember, containers are ephemeral which means that they should be disposable. Sure you can use the file system of containers to download a file and do some processing on it, for example. But your application shouldn’t expect that some file will always be around (except for config files mounted via Kubernetes secrets).

It’s preferable that your assets (CSS, JS, etc.) are compiled in the build steps of your images, never at runtime. Runtime is for receiving requests and sending responses back. Also, you should prefer using a CDN for your assets. You can easily hook your webpack builds to deploy to a CDN and have your assets much closer to your end-users, making the general feel of your application faster.

If you need a session to keep the context of your users around between requests, prefer using a backing service for that (see factor IV. Backing services). Many frameworks allow you to use different storage drivers for your sessions, such as Redis, Memcached, or even the database.

7. Port binding

Export services via port binding (source)

When we need to expose something from our containers such as a web UI, you can do so by binding it to a port.

For instance, the NodeJS example from before listens on port 3000:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-07-app-js

And in the Dockerfile we would have a EXPOSE entry:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-07-file-dockerfile

When running this container locally, you can explicitly tell it which port-binding you want:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-07-docker-run-sh

Now if I access http://localhost:3000 in the browser, I’ll see the message from my application.

Depending on the scheduler you use to run your containers, the port doesn’t matter that much. In Kubernetes, for instance, you can define in your service the port routing, so you could have other applications referencing your service via myapp-svc:80 while it routes to port 3000 in your PODs:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-07-k8s-service-yml

8. Concurrency

Scale out via the process model (source)

Your app must have a single process or a master process with some child processes, but the idea is that if you want to scale your application, you should spin up more containers (horizontally), not give it more resources (vertically). Even though your app might spin-up more processes inside the container (like PHP-FPM does), you should treat the container as a single unit, and the master process should be able to gracefully shut-down the children processes when terminated.

In a full-stack framework such as Laravel, the same application might have different entry points: (a) web; (b) worker; or (c) scheduler. This might feel like stretching in terms of 12 factors, as usually each of these aspects could be its own application, but this works really well in practice.

Your CI/CD pipeline will generate a single image version. That image will be used for running your application’s web, worker, and scheduler containers. Using Docker Swarm as an example, it would be something like this:

https://gist.github.com/tonysm/fd49af9f5524da66939f97b6f931ad30#file-08-docker-compose-yml

Now, when we deploy this stack, the processes count should be similar to the diagram from the 12factors site:

concurrency docker
Source: 12factor.net

It’s a bit different because in our case (the Docker Swarm example) the scheduler keeps exiting and restarting every 60 seconds instead of always running (which could also be a possibility). But the idea is the same. Quoting the definition again:

The process model truly shines when it comes time to scale out. The share-nothing, horizontally partitionable nature of twelve-factor app processes means that adding more concurrency is a simple and reliable operation. The array of process types and number of processes of each type is known as the process formation. (source)

The Orchestrator’s Scheduler (Kubernetes, Swarm, Nomads, etc.) should be the one restarting/deploying new versions of your application based on your described manifests.

9. Disposability

Maximize robustness with fast startup and graceful shutdown (source)

Your containers should be disposable, which means you can scale them up or down in your cluster at any given time. The share-nothing, ephemeral characteristic of containers makes scaling horizontally a real breeze.

Additionally, the PID 1 of your container must respond to POSIX signals. This means that if your container receives a SIGTERM, it should gracefully shut down.

As I said, if you are using S6 Overlay as your process supervisor, it will forward the signals to your processes which should handle it gracefully.

If you can’t gracefully shut down (or even if you can, but want to be extra careful), make things as idempotent as possible, and be sure to release your locks (or set a timeout) in case things go badly and your process crashes.

10. Dev/Prod parity

Keep development, staging, and production as similar as possible (source)

Since you are releasing containers, no matter where you deploy the images, it’s usually guaranteed that you will have parity between your environments.

You might remember that earlier I mentioned that you can have 2 different Dockerfiles, one for local, and one for your release.

It might feel like we are breaking parity, but I don’t think that’s entirely true. If you run PHP 7.4 in production, your local Dockerfile should also be based on PHP 7.4, but your local image might have different workflows and development tools.

The release image is supposed to run in isolation from everything else. Your local image has local dependencies such as your source code. You don’t want to rebuild your images every time you make a change in your code.

Sure, Docker Compose can map your local source files to the container via Volumes and that would solve this issue in particular. That’s true. But if you are running a Laravel application, for instance, you have other concerns while developing. You want a watcher on your assets file running webpack everytime you change something. Debug tools might also be installed locally, like xdebug, and you probably don’t want that in your release image. OS-level dependencies must be the same though since that’s the real parity and that includes PHP extensions. This can be solved by extracting the dependencies installation to a bash file and using that in both your local and production images, adding the local dependencies only on your local one.

That being said, try to keep as much parity as you can in your backing services. For instance, if your app uses MySQL in production, you should run MySQL locally and in the same version. If you have system or feature tests, you should also run those against MySQL (and it can be fast too).

Thanks to Docker and Docker Hub, you can find official images of almost all your beloved backing services to run locally.

11. Logs

Treat logs as event streams (source)

Don’t use file-based logs for your containers. Instead, prefer streaming the channels to stdout and stderr, which are available to all programs running on Unix systems.

If you rely on an official image, it might be the case that they have already configured it to the correct channels (see Nginx and PHP official images as examples).

If you are using a framework, make sure that you set their logging driver to use stdout and/or stderr as well. Laravel, for example, supports this configuration. By modifying configuration files (see factor III. Configuration), you can set LOG_CHANNEL=stderr (see here).

Ideally, some other software in your cluster is responsible for hooking into these streams and storing them somewhere so you can query it in a nicer way. Grafana has a tool called Loki that does that for us.

Additionally, you might also collect some telemetry data from your application. Nginx and PHP-FPM do offer very useful metrics for helping us with observability. For those, we should expose them via HTTP and have a tool like Prometheus to query that periodically.

Another option is using external services like Papertrail or NewRelic.

12. Admin Processes

Run admin/management tasks as one-off processes (source)

This one is easy to neglect when it comes to Docker containers. When we run a container, we usually get assigned to the root user inside the container. Ideally, the official images already deal with this issue for us.

For instance, PHP-FPM will use user www-data when it runs, even though you might start the container as root. Make sure you don’t run your applications as root unless you really need it. Even then, prefer to give root privilege to a very specific process/container, as you never know when a security breach will occur. Use the USER instruction if you can, as advised in the Docker documentation.

As you can see, the 12factor app manifesto maps really well to containers. By applying these guidelines on our applications, we can make our own lives easier when it comes to deploying them.

BTW, check our guide on getting rid of Docker Ports with Traefik.