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 Dockerfile
:
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 /usr/bin/java
.
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 java is located in /usr/bin , it’s just a symlink pointing to /usr/lib/jvm/default-jvm/jre/bin/java .
Hence, the /usr/lib folder needs to be exposed as well.
|
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 compose-this
image:
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.