title: Docker in Action - fitter, happier, more productive - Real Python
url: https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/
hash_url: 37adabe90396072db9cfdd99baa61cd0

<div class="center-text">
  <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/docker-in-action.png" alt="docker in action"/>
</div>

<p>With Docker you can easily deploy a web application along with it’s dependencies, environment variables, and configuration settings – everything you need to recreate your environment quickly and efficiently.</p>

<p>This tutorial looks at just that.</p>

<p><strong>Updated 02/28/2015</strong>: Added <a href="https://docs.docker.com/compose/">Docker Compose</a> and upgraded Docker and boot2docker to the latest versions.</p>

<hr/>


<p><strong>We’ll start by creating a Docker container for running a Python Flask application. From there, we’ll look at a nice development workflow to manage the local development of an app as well as continuous integration and delivery, step by step …</strong></p>

<blockquote><p>I (<a href="https://twitter.com/mikeherman">Michael Herman</a>) originally presented this workflow at <a href="https://www.pytennessee.org/"> PyTennessee</a> on February 8th, 2015. You can view the slides <a href="http://realpython.github.io/fitter-happier-docker/">here</a>, if interested.</p></blockquote>

<h2>Workflow</h2>

<ol>
<li>Code locally on a feature branch</li>
<li>Open a pull request on Github against the master branch</li>
<li>Run automated tests against the Docker container</li>
<li>If the tests pass, manually merge the pull request into master</li>
<li>Once merged, the automated tests run again</li>
<li>If the second round of tests pass, a build is created on Docker Hub</li>
<li>Once the build is created, it’s then automatically (err, automagically) deployed to production</li>
</ol>


<div class="center-text">
  <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/steps.jpg" alt="docker in action workflow"/>
</div>







<blockquote><p>This tutorial is meant for Mac OS X users, and we’ll be utilizing the following tools/technologies – Python v2.7.9, Flask v0.10.1, Docker v1.5.0, Docker Compose, v1.1.0, boot2docker 1.5.0, Redis v2.8.19</p></blockquote>

<p>Let’s get to it…</p>

<hr/>


<p>First, some Docker-specific terms:</p>

<ul>
<li>A <em>Dockerfile</em> is a file that contains a set of instructions used to create an <em>image</em>.</li>
<li>An <em>image</em> is used to build and save snapshots (the state) of an environment.</li>
<li>A <em>container</em> is an instantiated, live <em>image</em> that runs a collection of processes.</li>
</ul>


<blockquote><p>Be sure to check out the Docker <a href="https://docs.docker.com/">documentation</a> for more info on <a href="https://docs.docker.com/reference/builder/">Dockerfiles</a>, <a href="https://docs.docker.com/terms/image/">images</a>, and <a href="https://docs.docker.com/terms/container/">containers</a>.</p></blockquote>

<h2>Why Docker?</h2>

<p>You can truly mimic your production environment on your local machine. No more having to debug environment specific bugs or worrying that your app will perform differently in production.</p>

<ol>
<li>Version control for infrastructure</li>
<li>Easily distribute/recreate your entire development environment</li>
<li>Build once, run anywhere – aka The Holy Grail!</li>
</ol>


<h2>Docker Setup</h2>

<p>Since Darwin (the kernel for OS X) does not have the Linux kernel features required to run Docker containers, we need to install <a href="http://boot2docker.io/">boot2docker</a> – which is a <em>lightweight</em>t Linux distribution designed specifically to run Docker. In essence, it starts a small VM that’s configured to run Docker containers.</p>

<p>Create a new directory called “fitter-happier-docker” to house your Flask project.</p>

<p>Next follow the instructions from the guide <a href="https://docs.docker.com/installation/mac/">Installing Docker on Mac OS X</a> to install both Docker and the official boot2docker package.</p>

<p>Test the install:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>boot2docker version
</span><span class="line">Boot2Docker-cli version: v1.5.0
</span><span class="line">Git commit: ccd9032
</span></code></pre></td></tr></table></div></figure>


<h2>Compose Up!</h2>

<p><a href="http://docs.docker.com/compose/">Docker Compose</a> is an orchestration framework that handles the building and running of multiple services (via separate containers) using a simple <em>.yml</em> file. It makes it super easy to link services together running in different containers.</p>

<blockquote><p>Following along with me? Grab the code in a pre-Compose state from the <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/pre-compose">repository</a>.</p></blockquote>

<p>Start by installing the requirements via pip and then make sure Compose is installed:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>pip install docker-compose
</span><span class="line"><span class="nv">$ </span>docker-compose --version
</span><span class="line">docker-compose 1.1.0
</span></code></pre></td></tr></table></div></figure>


<p>Now let’s get our Flask application up and running along with Redis.</p>

<p>Add a new file called <em>docker-compose.yml</em> to the root directory:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
<span class="line-number">4</span>
<span class="line-number">5</span>
<span class="line-number">6</span>
<span class="line-number">7</span>
<span class="line-number">8</span>
<span class="line-number">9</span>
<span class="line-number">10</span>
<span class="line-number">11</span>
<span class="line-number">12</span>
<span class="line-number">13</span>
</pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">web</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="l-Scalar-Plain">build</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">web</span>
</span><span class="line">    <span class="l-Scalar-Plain">volumes</span><span class="p-Indicator">:</span>
</span><span class="line">        <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">web:/code</span>
</span><span class="line">    <span class="l-Scalar-Plain">ports</span><span class="p-Indicator">:</span>
</span><span class="line">        <span class="p-Indicator">-</span> <span class="s">"80:5000"</span>
</span><span class="line">    <span class="l-Scalar-Plain">links</span><span class="p-Indicator">:</span>
</span><span class="line">        <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">redis</span>
</span><span class="line">    <span class="l-Scalar-Plain">command</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">python app.py</span>
</span><span class="line"><span class="l-Scalar-Plain">redis</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="l-Scalar-Plain">image</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">redis:2.8.19</span>
</span><span class="line">    <span class="l-Scalar-Plain">ports</span><span class="p-Indicator">:</span>
</span><span class="line">        <span class="p-Indicator">-</span> <span class="s">"6379:6379"</span>
</span></code></pre></td></tr></table></div></figure>


<p>Here we add the services that make up our stack:</p>

<ol>
<li><strong>web</strong>: First, we build the image from the “web” directory and then mount that directory to the “code” directory within the Docker container. The Flask app is ran via the <code>python app.py</code> command. This exposes port 5000 on the container, which is forwarded to port 80 on the host environment.</li>
<li><strong>redis</strong>: Next, the Redis service is built from the Docker Hub “Redis” <a href="https://registry.hub.docker.com/_/redis/">image</a>. Port 6379 is exposed and forwarded.</li>
</ol>


<p>Did you notice the Dockerfile in the “web” directory? This file is used to build our image, starting with an Ubuntu base, the required dependencies are installed and the app is built.</p>

<h2>Build and run</h2>

<p>With one simple command we can build the image and run the container:</p>

<figure class="code"><figcaption><span/></figcaption></figure>




<div class="center-text">
  <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/figup.png" alt="fig up"/>
</div>







<p>This command builds an image for our Flask app, pulls the Redis image, and then starts everything up.</p>

<p>Grab a cup of coffee. Or two. This will take some time the first time you build the container. That said, since Docker caches each step (or <em><a href="https://docs.docker.com/terms/layer/">layer</a></em>) of the build process from the Dockerfile, rebuilding will happen <em>much</em> quicker because only the steps that have <em>changed</em> since the last build are rebuilt.</p>

<blockquote><p>If you do change a line/step/layer in your Dockerfile, it will recreate/rebuild everything in that line – so be mindful of this when you structure your Dockerfile.</p></blockquote>

<p>Docker Compose brings each container up at once in parallel. Each container also has a unique name and each process within the stack trace/log is color-coded for readability.</p>

<p>Ready to test?</p>

<p>Open your web browser and navigate to the IP address associated with the <code>DOCKER_HOST</code> variable – i.e., <a href="http://192.168.59.103/,">http://192.168.59.103/,</a> in this example. (Run <code>boot2docker ip</code> to get the address.)</p>

<p>You should see the text, “Hello! This page has been seen 1 times.” in your browser:</p>

<div class="center-text">
  <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/test.png" alt="test flask app running on docker"/>
</div>







<p>Refresh. The page counter should have incremented.</p>

<p>Kill the processes (Ctrl-C), and then run the following command to run the process in the background.</p>

<figure class="code"><figcaption><span/></figcaption></figure>


<p>Want to view the currently running processes?</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
<span class="line-number">4</span>
<span class="line-number">5</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>docker-compose ps
</span><span class="line">            Name                          Command             State              Ports
</span><span class="line">--------------------------------------------------------------------------------------------------
</span><span class="line">fitterhappierdocker_redis_1     /entrypoint.sh redis-server   Up      0.0.0.0:6379-&gt;6379/tcp
</span><span class="line">fitterhappierdocker_web_1       python app.py                 Up      0.0.0.0:80-&gt;5000/tcp, 80/tcp
</span></code></pre></td></tr></table></div></figure>


<blockquote><p>Both processes are running in a different container, connected via Docker Compose!</p></blockquote>

<h2>Next Steps</h2>

<p>Once done, kill the processes via <code>docker-compose stop</code>, then run <code>boot2docker down</code> to gracefully shutdown the VM. Commit your changes locally, and then push to Github.</p>

<p>So, what did we accomplish?</p>

<p>We set up our local environment, detailing the basic process of building an <em>image</em> from a <em>Dockerfile</em> and then creating an instance of the <em>image</em> called a <em>container</em>. We tied everything together with Docker Compose to build and connect different containers for both the Flask app and Redis process.</p>

<p><strong>Now, let’s look at a nice continuous integration workflow powered by <a href="https://circleci.com/">CircleCI</a></strong>.</p>

<blockquote><p>Still with me? You can grab the updated code from the <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/added-compose">repository</a>.</p></blockquote>

<h2>Docker Hub</h2>

<p>Thus far we’ve worked with Dockerfiles, images, and containers (abstracted by Docker Compose, of course).</p>

<p>Are you familiar with the Git workflow? Images are like Git repositories while containers are similar to a cloned repository. Sticking with that metaphor, <a href="https://hub.docker.com/">Docker Hub</a>, which is repository of Docker images, is akin to Github.</p>

<ol>
<li>Signup <a href="https://hub.docker.com/account/signup/">here</a>, using your Github credentials.</li>
<li>Then add a new automated build. And add your Github repo that you created and pushed to. Just accept all the default options, except for the “Dockerfile Location” – change this to “/web”.</li>
</ol>


<p>Once added, this will trigger an initial build. Make sure the build is successful.</p>

<h3>Docker Hub for CI</h3>

<p>Docker Hub, in itself, acts as a continuous integration server since you can configure it to create an <a href="https://docs.docker.com/userguide/dockerrepos/#automated-builds">automated build</a> every time you push a new commit to Github. In other words, it ensures you do not cause a regression that completely breaks the build process when the code base is updated.</p>

<blockquote><p>There are some drawbacks to this approach – namely that you cannot push (via <code>docker push</code>) updated images directly to Docker Hub. Docker Hub must pull in changes from your repo and create the images itself to ensure that their are no errors. Keep this in mind as you go through this workflow. The Docker documentation is not clear with regard to this matter.</p></blockquote>

<p>Let’s test this out. Add an assert to the test suite:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
</pre></td><td class="code"><pre><code class="python"><span class="line"><span class="bp">self</span><span class="o">.</span><span class="n">assertNotEqual</span><span class="p">(</span><span class="n">four</span><span class="p">,</span> <span class="mi">102</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<p>Commit and push to Github to generate a new build on Docker Hub. Success?</p>

<p><strong>Bottom-line:</strong> It’s good to know that if a commit does cause a regression that Docker Hub will catch it, but since this is the last line of defense before deploying (to either staging or production) you ideally want to catch any breaks before generating a new build on Docker Hub. Plus, you also want to run your unit and integration tests from a <em>true</em> continuous integration server – which is exactly where CircleCI comes into play.</p>

<h2>CircleCI</h2>

<div class="center-text">
  <img class="no-border" src="https://realpython.com/images/blog_images/docker-in-action/circleci.png" alt="circleci"/>
</div>







<p><a href="https://circleci.com/">CircleCI</a> is a continuous integration and delivery platform that supports testing within Docker containers. Given a Dockerfile, CircleCI builds an image, starts a new container, and then runs tests inside that container.</p>

<p>Remember the workflow we want? <a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/#workflow">Link</a>.</p>

<p>Let’s take a look at how to achieve just that…</p>

<h3>Setup</h3>

<p>The best place to start is the excellent <a href="https://circleci.com/docs/getting-started">Getting started with CircleCI</a> guide…</p>

<p>Sign up with your Github account, then add the Github repo to create a new project. This will automatically add a webhook to the repo so that anytime you push to Github a new build is triggered. You should receive an email once the hook is added.</p>

<p>Next we need to add a configuration file to the root folder of repo so that CircleCI can properly create the build.</p>

<h3><em>circle.yml</em></h3>

<p>Add the following build commands/steps:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
<span class="line-number">4</span>
<span class="line-number">5</span>
<span class="line-number">6</span>
<span class="line-number">7</span>
<span class="line-number">8</span>
<span class="line-number">9</span>
<span class="line-number">10</span>
<span class="line-number">11</span>
<span class="line-number">12</span>
</pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">machine</span><span class="p-Indicator">:</span>
</span><span class="line">  <span class="l-Scalar-Plain">services</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">docker</span>
</span><span class="line">
</span><span class="line"><span class="l-Scalar-Plain">dependencies</span><span class="p-Indicator">:</span>
</span><span class="line">  <span class="l-Scalar-Plain">override</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">pip install -r requirements.txt</span>
</span><span class="line">
</span><span class="line"><span class="l-Scalar-Plain">test</span><span class="p-Indicator">:</span>
</span><span class="line">  <span class="l-Scalar-Plain">override</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">docker-compose run -d --no-deps web</span>
</span><span class="line">    <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">python web/tests.py</span>
</span></code></pre></td></tr></table></div></figure>


<p>Essentially, we create a new image, run the container, then run your unit tests.</p>

<blockquote><p>Notice how we’re using the command <code>docker-compose run -d --no-deps web</code>, to run the web process, instead of <code>docker-compose up</code>. This is because CircleCI already has Redis <a href="https://circleci.com/docs/environment#databases">running</a> and available to us for our tests. So, we just need to run the web process.</p></blockquote>

<p>With the <em>circle.yml</em> file created, push the changes to Github to trigger a new build. <em>Remember: this will also trigger a new build on Docker Hub.</em></p>

<p>Success?</p>

<p>Before moving on, we need to change our workflow since we won’t be pushing directly to the master branch anymore.</p>

<h3>Feature Branch Workflow</h3>

<blockquote><p>For these unfamiliar with the Feature Branch workflow, check out <a href="https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow">this</a> excellent introduction.</p></blockquote>

<p>Let’s run through a quick example…</p>

<h3>Create the feature branch</h3>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git checkout -b circle-test master
</span><span class="line">Switched to a new branch <span class="s1">'circle-test'</span>
</span></code></pre></td></tr></table></div></figure>


<h3>Update the app</h3>

<p>Add a new assert in <em>tests.py</em>:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
</pre></td><td class="code"><pre><code class="python"><span class="line"><span class="bp">self</span><span class="o">.</span><span class="n">assertNotEqual</span><span class="p">(</span><span class="n">four</span><span class="p">,</span> <span class="mi">60</span><span class="p">)</span>
</span></code></pre></td></tr></table></div></figure>


<h3>Issue a Pull Request</h3>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git add web/tests.py
</span><span class="line"><span class="nv">$ </span>git commit -m <span class="s2">"circle-test"</span>
</span><span class="line"><span class="nv">$ </span>git push origin circle-test
</span></code></pre></td></tr></table></div></figure>


<p>Even before you create the actual pull request, CircleCI starts creating the build. Go ahead and create the pull request, then once the tests pass on CircleCI, press the Merge button. Once merged, the build is triggered on Docker Hub.</p>

<h3>Refactoring the workflow</h3>

<p>If you jump back to the overall workflow at the <a href="https://realpython.com/blog/python/docker-in-action-fitter-happier-more-productive/#workflow">top of this post</a>, you’ll see that we don’t actually want to trigger a new build on Docker Hub until the tests pass against the master branch. So, let’s make some quick changes to the workflow:</p>

<ol>
<li>Open your repository on Docker Hub, and then under <em>Settings</em> click <em>Automated Build</em>.</li>
<li>Uncheck the Active box: “When active we will build when new pushes occur”.</li>
<li>Save.</li>
<li>Click <em>Build Triggers</em> under <em>Settings</em></li>
<li>Change the status to on.</li>
<li>Copy the example curl command – i.e., <code>$ curl --data "build=true" -X POST https://registry.hub.docker.com/u/mjhea0/fitter-happier-docker/trigger/84957124-2b85-410d-b602-b48193853b66/</code></li>
</ol>


<p>Now add the following code to the bottom of your <em>circle.yml</em> file:</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
<span class="line-number">4</span>
<span class="line-number">5</span>
</pre></td><td class="code"><pre><code class="yaml"><span class="line"><span class="l-Scalar-Plain">deployment</span><span class="p-Indicator">:</span>
</span><span class="line">  <span class="l-Scalar-Plain">hub</span><span class="p-Indicator">:</span>
</span><span class="line">    <span class="l-Scalar-Plain">branch</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">master</span>
</span><span class="line">    <span class="l-Scalar-Plain">commands</span><span class="p-Indicator">:</span>
</span><span class="line">      <span class="p-Indicator">-</span> <span class="l-Scalar-Plain">$DEPLOY</span>
</span></code></pre></td></tr></table></div></figure>


<p>Here we fire the <code>$DEPLOY</code> variable <em>after</em> we merge to master <em>and</em> the tests pass. We’ll add the actual value of this variable as an environment variable on CircleCI:</p>

<ol>
<li>Open up the <em>Project Settings</em>, and click <em>Environment variables</em>.</li>
<li>Add a new variable with the name “DEPLOY” and paste the example curl command (that you copied) from Docker Hub as the value.</li>
</ol>


<p>Now let’s test.</p>

<figure class="code"><figcaption><span/></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class="line-number">1</span>
<span class="line-number">2</span>
<span class="line-number">3</span>
</pre></td><td class="code"><pre><code class="sh"><span class="line"><span class="nv">$ </span>git add circle.yml
</span><span class="line"><span class="nv">$ </span>git commit -m <span class="s2">"circle-test"</span>
</span><span class="line"><span class="nv">$ </span>git push origin circle-test
</span></code></pre></td></tr></table></div></figure>


<p>Open a new pull request, and then once the tests pass on Circle CI, merge to master. Another build is trigged. Then once the tests pass again, a new build will be triggered on Docker Hub via the curl command. Nice.</p>

<blockquote><p>Remember how I said that I configured Docker Hub to pull updated code to create a new image? Well, you could also set it up to where you can push images directly to Docker Hub. So once these test pass, you could simply push the image to update Docker Hub and then deploy to staging or production, directly from CircleCI. Since I have it set up differently, I handle the push to production from Docker Hub, not CircleCI. There’s positives and negatives to both approaches, as you will soon find out.</p></blockquote>

<h2>Conclusion</h2>

<p>So, we went over a nice development workflow that included setting up a local environment coupled with continuous integration via <a href="https://circleci.com/">CircleCI</a> (steps 1 through 6):</p>

<ol>
<li>Code locally on a feature branch</li>
<li>Open a pull request on Github against the master branch</li>
<li>Run automated tests against the Docker container</li>
<li>If the tests pass, manually merge the pull request into master</li>
<li>Once merged, the automated tests run again</li>
<li>If the second round of tests pass, a build is created on Docker Hub</li>
<li>Once the build is created, it’s then automatically (err, automagically) deployed to production</li>
</ol>


<p>What about the final piece – delivering this app to the production environment (step 7)? You can actually follow another one of my Docker blog <a href="https://blog.rainforestqa.com/2015-01-15-docker-in-action-from-deployment-to-delivery-part-3-continuous-delivery">posts</a> to extend this workflow to include delivery.</p>

<p>Comment below if you have questions. Grab the final code <a href="https://github.com/realpython/fitter-happier-docker/releases/tag/final-code">here</a>. Cheers!</p>

<hr/>


<p><strong>If you have a workflow of your own, please let us know. I am currently experimenting with <a href="https://github.com/saltstack/salt">Salt</a> as well as <a href="https://www.tutum.co/">Tutum</a> to better handle orchestration and delivery on Digital Ocean and Linode.</strong></p>
