Docker-writing a smaller image with multi stage builds for .NET core
I’ve been using docker for playing around with my dinky website, but the DockerFile/image has always been a bit brute forcey. It’s time to explore a somewhat more effective DockerFile!
Docker — writing a smaller image with multi stage builds for. NET core.
Docker Overview
Docker is a method of building applications/infrastructure/code within a container; a container being a self contained piece of software with all dependencies needed to run an application.
Though not directly related to a build server, they do have some overlap in some of the problems they try to solve. When utilizing either docker or a build server, your build process and its dependencies need to be codified… in code. The idea is that you’re writing “docker code” in order to describe the steps to build and deploy your app. This is very similar to using a build server in that you can be sure that any developer or server will be able to build or run your application code, without the hassle of installing all of your applications dependencies, as those dependencies are referenced within the docker “code” itself. (Note, you still need to have docker installed, and there are likely a few other caveats, especially when it comes to injecting variables into your docker containers.)
Current Image
The current image I’m using is quite small (code length wise), and due to that fact builds take longer than they should. This is due simply to the fact there are no real “checkpoints” in my build process. I’ll try to explain more about that while walking through my base image:
dnc2.1.401-v1-base
1 |
|
dnc2.1.401-v1-node
1 |
|
KritnerWebsite.DockerFile
1 |
|
Issues with Current Image
- Not really using “multi stage builds” (multiple “from” statements). I’m using a few different images, but it’s all being rolled up into the final image. This means I’m running a final image with a whole lot more “stuff” than what should be needed.
- Due to the way I’m building my final
KritnerWebsite.Dockerfile
based off of my other images, it’s not very flexible when it comes to upgrading which sdk I’m using. I currently need to updatednc2.1.401-v1-base
, rebuilddnc2.1.401-v1-node
, then rebuild my actual website image. - Though related to the previous two points, I thought it deserved its own: I’m currently installing a LOT on top of the dnc image — things like sudo, node, performing OS level updates. Working with “separate” images for building dotnet core code, and running dotnet core, would help avoid some of this.
Refactoring my DockerFile
A Better Image Template (Thanks GaProgMan)
GaProgManhas worked a bit with docker, and had a few tips for me with a multi-step build process he gave me a few months ago for reference (yes, I’m just getting to this now):
1 |
|
Adapting the template to my build
I don’t want to copy exactly off of GaProgMan’s sample, luckily he commented it very well, so I’d know what’s happening. The most important thing I’m shooting for is creating more layers. These layers are important for ensuring more things will be cached; so not rebuilt (necessarily) with every build of the DockerFile
.
First things first — I know I can cut down on my image size by utilizing two separate base images throughout the docker file:
- SDK — for building
- Runtime — for running
Previously, I was using only the SDK, which blows up my final image size by quite a bit — my images’ current size is 2.23 GB as per docker images (yeesh!).
So for the two images — sdk and runtime:
1 |
|
In the above we’re running a few commands on the base images for the purpose of installing nodejs — which we’ll need both for building and running the angular app; at least I’m pretty sure it’s needed for both right?
1 |
|
Next, we’ll do the dotnet restore on the single copied project file — the reasoning behind this was pretty well explained in the above example, but I didn’t really realize it worked this way until seeing it in GaProMan’s comments. Basically, this restored “layer” can be cached, and never “rebuilt” unless something in the dependencies changes, saving on time when rebuilding our docker image!
1 |
|
Same idea in the above, but for npm packages instead of .net dependencies.
1 |
|
In the above, I’m copying the entirety of the buildable source directory, and performing a build with the .net CLI. Special note that the --no-restore
option is being used as a restore operation was performed previously.
1 |
|
Here, in a similar idea to the build layer, we’re performing a publish; making sure not to restore or build as both have already been completed.
Finally:
1 |
|
In the above we’re copying our built application from the publish image, into a new “final” image that was based off of “base” (the run time).
The new DockerFile
The new DockerFile looks like this in its entirety:
1 |
|
Now that the image is built, I can run it like normal to test it out:
1 |
|
Huh, it actually seems to have worked! :D
Now I can push the image up to dockerhub, and pull it down on my server.
1 |
|
Now, to see the difference in size between the previous image and the current, I run docker images and am presented with:
So we went from a chonky 2.23GB to a cool 417MB, nice!
Wrap Up
Thanks to GaProgMan for pointing me in the right direction for making my docker image more useful. Code for this post can be found:
Related:
- How to setup your website for that sweet, sweet HTTPS with Docker, Nginx, and letsencrypt
- And it’s like… what’s the deal with build servers?
- Reworks
DockerFile
for better multi stage support by Kritner · Pull Request #27 · Kritner/KritnerWebsite - Docker
Docker-writing a smaller image with multi stage builds for .NET core