December 25, 2020#technical

Simplifying App Deploys with Docker

If you recall, I've been writing Dokku-replacement tools for a couple of purposes now: my website, and some CS 61A infrastructure. It occurred to me that this is a fairly common use case, and there's no reason for me to not turn it into a reusable library. It'll make updating my website's buildserver as well as 61A's buildserver far more convenient, and new features will end up in both places. So I did!

Introducing DNA. DNA is a Python library that abstracts away the nuances of Docker and Nginx to simplify the process of deploying apps. A lot. You simply tell it the name of the image to deploy and which port the public-facing app runs on, and DNA handles the rest. Then, you tell it to proxy a domain to it, and DNA automatically writes the Nginx configuration for you, uses Certbot to sign an SSL certificate, and makes your app accessible on the internet.

Not only that, but it also allows you to manage various apps by starting or stopping them, proxying additional domains to them, unproxying proxied domains, updating the service with little downtime (in fact, one of my goals is to make the transition happen with no downtime), viewing multiple types of service logs (currently includes Docker logs, Nginx access logs, and Nginx error logs; I'm working on making build logs separate from DNA's internal logs, but for the time being, build logs are visible in DNA's internal logs), and deleting the services entirely.

To show you how straightforward a basic DNA app is, let's walk through the quickstart. Here's the file from that guide.

First, we import the relevant libraries. We're going to wrap DNA in a Flask server for this demo, so we'll import Flask.

from flask import Flask, jsonify
from dna import DNA

Now, we'll create a Flask app as well as a DNA app.

app = Flask(__name__)
dna = DNA("demo_dna")

In practice, apps will be far more complicated than the one we're making here. Right now, we'll just add a single method, one that deploys a Docker image with a specific container name. We will specify the image, the container's name, and which port the front-facing app runs on. After we've pulled the image and deployed it, we'll attach {name} to that domain so we can access it.

def deploy(image, name):
    """Pulls the ``image`` and deploys it to a service called ``name``.
    Sets up a webserver configuration to forward ``name`` to
    the deployed app. Assumes the front-end runs on port 80.

    :param image: the name/url of the image to pull and deploy
    :type image: str
    :param name: the name to give this service
    :type name: str
    # pull the image from the relevant container repository
    # deploy the image with the given name, opening up port 80 for traffic
    dna.run_deploy(name, image, port="80")
    # proxy to the container, so we can access the app
    dna.add_domain(name, f"{name}")

    # return success!
    return jsonify({
        "success": True,
        "url": f"{name}",

if __name__ == "__main__":"")

Now, if you start the server and visit /deploy/ubuntu/utest, a container running Ubuntu will be deployed at! The only issue with that example, of course, is that a Ubuntu container would simply exit since there's no default running command. But, when coupled with an image that does run something, isn't this much nicer than having to dig through the Docker and Nginx options yourself? And the cherry on top is that if you do need more complicated configurations, run_deploy captures any excess keyword arguments as options to pass into Docker, and add_domain has a proxy_set_header argument that allows you to set various headers in the Nginx config! More options coming soon :)

This is a work in progress, and I intend to keep adding features to it for as long as possible. Feel free to check out the documentation, the Github repo, or the PyPI project!