|This post is neither a recommendation, nor even a suggestion. It’s just me toying with an idea: Implement it at your own risk!|
The main benefit of Docker containers is that they are self-contained. For developers, that means one just needs to inherit from the desired Docker image that contains the necessary required dependencies, and presto, one can build one’s application deliver it to production. Most of the times, the process is pretty straightforward. Containerization allows scaling on unprecedented scale.
However, the downside of this self-containedness is that updating the parent image(s) becomes a nightmare. The process is the following:
- The Dockerfile is updated with the new parent image
- A new image is built
- The resulting image is made available on a Docker registry
- Finally, it’s pulled from said registry on the production machine, and started
While that might be acceptable for a single image once in a while, it’s definitely not feasible with the update frequency increasing, as well as the number of containerized applications involved.
Now, let’s consider the existing tradeoff of self-containedness vs flexibility, and relax the constraint. Instead of inheriting from the image, one could expose the folder that contains the required dependencies: it’s exactly the same OOP principle of Favor composition over inheritance! With that design, one just needs to replace the composed image to update the required binaries.
As an example, I’ll create a simple Java application incorporating this design. I assume a Maven project that generates an executable JAR. Here’s the relevant
FROM maven:3.6.0-alpine as build COPY src src COPY pom.xml . RUN mvn package FROM alpine:3.8 COPY --from=build target/composition-example-1.0-SNAPSHOT.jar . ENTRYPOINT ["sh", "-c", "/usr/bin/java -jar composition-example-1.0-SNAPSHOT.jar"]
It’s a multi-stage build that first builds the Maven project, and then runs it via
Note that standard Dockerfiles would inherit from a base JRE image, such as
openjdk:8-jre-alpine. Here, the second stage inherits from the base
alpine image, there’s no
java executable available. Hence, running the built Docker image will fail:
$ docker build -t compose-this . $ docker run compose-this -jar: line 1: java: not found
To fix that, let’s create a Docker image that exposes its
java executable in a volume:
FROM openjdk:8-jre-alpine VOLUME /usr/bin VOLUME /usr/lib
| While |
Build and run that image:
$ docker build -t myjava:8 . $ docker run --name java myjava:8
At that point, it becomes possible to bind the volumes from the
java container to new containers running the
docker run -it --volumes-from java compose-this
Not only it’s now much easier to update the dependent JRE, an added benefit is that the application image has been drastically reduced compared to its standalone image: it contains only the JAR.
A question could arise: what would be the difference between this setup and the uncontainerized one, with plain JARs and a shared JRE on the filesystem? This allows orchestration i.e. Kubernetes. While the same could be achieved with configuration management tools e.g. Puppet or Chef, Kubernetes is becoming the de facto platform to deploy to.
Docker and containerization technologies have been around only for some time. It will probably take some time - as well as some trials and errors - to understand how to make the most of it in one’s own context. This post exposes one of the many options available.