Enthusiasm for Docker continues to grow at a remarkable rate. From a datacenter point of view, Docker offers the potential for a large increase in the number of applications that can run on a single piece of hardware. That alone is reason enough for IT, cloud operations, and datacenter folks to take a serious look at Docker.
That’s all well and good, but as a developer you may be asking: “what’s in it for me?”. It turns out that Docker offers some significant benefits for you too. If your application will be deployed using Docker, you’ll definitely be ahead of the game if you start using Docker on your laptop during development. Even if you don’t (yet) intend to deploy using Docker, there are many benefits worthy of your consideration.
Wouldn’t it be great if everyone on your development team was able to run your app in exactly the same way? How does this sound:
- App setup is codified and easily shared. Integration, test, and development environments are spun up quickly without the need to follow a multi-page (likely out of date) document detailing how to install and configure. A new hire can be running your app locally in a blink instead of spending her first several days doing setup.
- Everyone runs the same version of the OS and all key development tools. There are no “flag days” where everyone has to update parts of the toolchain before they can move forward.
- Your local environment doesn’t fail in a way that leaves everyone else scratching their heads and saying “Weird. It works fine on my machine.”
Well, Docker brings those goals within reach. If you build your app into one or more Docker images and push the images to a registry, they can be pulled from the registry and run by all of your team members. If you’ve ever tried producing a cookie-cutter “development virtual machine” and sharing a .vmdk file among your team, this isn’t quite the same thing. Docker images are typically much smaller than virtual disk images, and Docker containers start much faster than virtual machines. Also, you can modify the common image after the fact and have everyone pull the new version. That’s much easier than trying to propagate updates among a number of extant VMs.
Docker images are self-contained. This ensures that everyone on your team runs a consistent toolset. So, if you have a Python app, your image will contain the Python command along with all of the packages used by your app (the ones you got from pip or easy_install) and the actual app code developed by your team. If you have a Node.js app, your image will contain commands needed to run your app (e.g., node, npm, gulp, etc.) along with your app code (package.json, node_modules, plus all the source you’ve developed). If you have a Java app, your image will contain a Java VM along with all of the parts of your runtime classpath (all the .jar and .war files that comprise your app and its dependencies).
Docker images are immutable so you can be certain that everyone who runs your app from a given image is running with the same bits. You will typically want to build your images so that per-instance configuration data (e.g., database info like connection URL) is configured externally (e.g., by using environment variables). That way it’s not necessary to open up the image just to connect to a test vs. a production database. Also, be aware that you can arrange for code running in a container to access local data by mounting a local directory as a data volume.
In the (hopefully rare) event that someone does need to make a local change to an image, your “official” app image can be extended by adding an image layer which contains only a set of differences (file additions, modifications or deletions) from the preceding layer. See docker documentation for more information about image layers.
Complete App Running On Your Desktop
With Docker it is quite feasible to run large multi-component applications completely on a single laptop. Docker images are generally small enough, and containers are generally lightweight enough that it’s common to run many tens of containers concurrently on a moderately powerful laptop. Likewise it’s common to store many tens of images on that same laptop. Note that Docker Hub has official repositories for a number of common databases (e.g., MySQL, MongoDB). So even if your app uses a hosted database, it’s pretty simple to run a local surrogate and configure your app to connect appropriately.
Let’s say hypothetically that your developers don’t generally run your complete app. For various, likely valid, reasons they work in their respective areas and validate with unit tests. This suggests that you would have an integration environment somewhere further downstream in your development process. That means integration can’t happen until code changes have reached the integration environment. In addition to being delayed, integration is probably more complex by virtue of the fact that multiple changes are being seen together for the first time in the integration environment. If developers can put all the pieces together in their home environment it’s much easier to isolate changes and iterate through code/test cycles.
Switch Between Projects
Do your work days ever cycle between multiple projects or multiple versions of a single project? It can be difficult to manage the environments in such cases. If you use different toolsets (i.e., different implementation languages, different databases, etc.), you can easily waste a lot of your local disk space on static tool installations. If you use different versions of the same toolset you get the added issue of maintaining separate path and environment setup procedures.
A number of common tools are available as free, public Docker images. For example, on Docker Hub you can find images for most of the common programming languages, along with instructions (among other things):
- How to parameterize
- How to use for compilation
- How to extend for deployment
Using a Docker container instead of a locally installed tool saves you the trouble of installing/configuring the tool, and allows you to easily get rid of a version if desired. If you no longer need/use it you can simply run docker image rm. If you’ve suddenly run short of disk space, you can delete images you’re not currently using, secure in the knowledge that they’re just a docker pull away the next time you need them.
Note that the command for compiling with a Docker image is more complex than what you’d use for a local compilation, so if you move to using Docker containers for compilation you will probably want to construct aliases or scripts to avoid a lot of extra keystrokes. For example, with a traditional Java implementation, you might compile and run from a shell like so (where % is the shell prompt):
% javac Hello.java % java Hello
With your Java tools in a container the same commands would look more like this:
% docker run --rm -v "$PWD":/usr/src/myapp \ -w /usr/src/myapp openjdk:7-jdk-alpine javac Hello.java % docker run --rm -v "$PWD":/usr/src/myapp \ -w /usr/src/myapp openjdk:7-jdk-alpine java Hello
The ability to easily run your entire app locally is a great enabler for testing. You can develop and run full-featured integration tests locally. Thus your local build/test runs can execute the same tests as your CI/CD builds. This increases the likelihood that your CI/CD environment stays clean rather than being continually derailed by failures that could/should have been caught further upstream.
It’s usually straight forward to seed a local database container with sample data for test runs. It’s also fast and easy to drop the DB container and start up a clean new one. If you’ve ever tried to run tests against a live database, you can see the attraction of this. It’s relatively simple to get the DB initialized correctly for tests, and there’s no concern about making sure that proper cleanup has happened at the end of the test run. That generally allows for simpler test implementations and faster test execution.
If the interfaces in a multi-container app are sufficiently defined, you might find it pretty simple to replace one or more services with mock versions. This allows development of mutually dependent components to proceed in parallel once they have agreed on interface definition. In our experience we have seen less need for mock implementations since we are now able to run the entire application on our development machines.
Docker offers a lot of benefits in operational scenarios. From a developer’s perspective, there are also significant deployment benefits to consider.
If you use Docker throughout development and then deploy with Docker too, the differences between your development environment and your deployment environment are practically nil. This really minimizes the potential for any unwelcome surprises at deployment time.
In some ways, a Docker-based application is a “write-once, run anywhere” kind of proposition. Obviously this doesn’t play out quite the same way as the old Java promise, but there are similar benefits for a developer (and of course your app need not be coded in Java to realize the benefit). In general, you just don’t need to worry about the potential for deployment on different platforms. You simply build your Docker image, secure in the knowledge that any Docker engine can run it.
Deployment with Docker also gives you a good opportunity to minimize the attack surface of your app. The isolation provided by the runtime container is significant by itself. For our purposes, it’s reasonable to think of a container like an independent machine. You control which ports are externally visible, other containers it has access to, where data is stored, etc. Further, you (as the builder of the image) have full control over the contents of the image. Obviously there’s no reason to include programs that your app doesn’t use. This is an important way in which Docker deployment differs from VMs. It’s not uncommon for deployed VMs to contain unused programs and services by virtue of having installed an OS distribution. With a VM, you’re expected to “harden” the image for deployment by removing things you don’t want/need. Docker has more of an “opt-in” model in which your image contains only what you’ve explicitly put in it. Make sure you understand the contents of the base image that you get via the FROM directive in your Dockerfile. You have complete control of image content beyond the base image via the ADD and COPY directives.
Docker has a lot to offer to both deployers and developers. Maximum benefit comes from using it for both. I hope you’ll check it out — and visit us at yipee.io to learn about easier ways to model and share the definitions of your multi-service applications.