Docker is a great tool to for running applications without worrying about dependencies, but it can also be used to build your application. In this post I'm gonna share a nice technique I've used to build and run a web app with Docker and stopped worrying about dependencies.
Building an application usually requires a lot more dependencies than just running it and most languages and frameworks offer a way to build your application bundling everything you need to run it.
As an example a Java application can be compiled to a single
.jar file, Go apps can be compiled to a binary and even applications in Ruby can be bundled in a SO package (like rpm). This makes it a lot easier to run the application, and even more so if you use Docker.
Once your application is bundled, you can just copy it to a Docker image and have docker run it for you - and only worry about installing Docker itself. What about actually building the whole app in a Docker image so you don't even need to worry about the dependencies even on your machine (or on a CI server)?
The main problem with building your app on the same image that will be used to run it is that this image will have to have everything required for both building and running.
Every command you add in a
Dockerfile will add a layer to your image. Think of a Docker image as a git repository, the very first
FROM command is the initial commit for that repo, each command after that is like a new commit that will potentially increase the size of your repository. The more commands you have in a
Dockerfile the bigger your image might be.
So, how about using two different images? One for building and another one for running?
I was working on a small web app (using Java and Spring Boot) and while searching for best practices around docker I ran into this very useful video. It talks about different ways to reduce the size of your image (which is specially useful in the context of Kubernetes).
So I decided to apply these techniques to build and deploy this Java/Spring Boot web app, using one image to build the
.jar file and another one to run it. We used gradle on this project so my first step was to use the gradle base image to build the app.
FROM gradle:jdk8 AS build-env # Setup ARGs and ENVs # Some RUN commands # Copy the code COPY --chown=gradle:gradle . . # Some more RUN commands # Finally, build the app RUN ["./gradlew", "build"]
This image is actually pretty big. First of all it uses a base image that comes with a lot more stuff (comparing to the
-alpine alternative), because I actually need tools like
curl and others that I'd have to install myself if I had used a thiner version.
And that's actually the point of using a build image, you don't need to worry about what's there because it will be discarded once the final image is built.
Now I can define the image that will run the application:
FROM openjdk:8-alpine COPY entrypoint.sh entrypoint.sh COPY --from=build-env /home/gradle/build/libs/my-app-0.0.0.jar my-app.jar ENTRYPOINT [ "sh", "entrypoint.sh" ]
This is a pretty thin image, it uses the
8-alpine version of
openjdk, which is much smaller and does the job well, since Java is pretty much the only runtime dependency.
Both of these declaration live on the same
Dockerfile, note how the build image has
FROM ... AS build-env at the beginning. This will create a temporary image with a
build-env alias and since Docker only considers the last
FROM statement to be the actual image it will be discard afterwards. Then on the runtime image it copies from the build image with
And there you go, building and running an app using a single
Dockerfile while optimizing for each context. Using this pattern can even help simplify your CI by only having to install Docker on the agents.
Thanks for taking the time to read this and hope it helps you to improve your own images :)