How to make Docker images even smaller
symflower.com
symflower.com
> Use a smaller base image or libc
By default, Nix will build container images from scratch. No base image (although I guess you can add one as a layer to your manifest JSON, if you like).
> Make the image context smaller
Nix tracks dependencies very precisely. Our container images will only contain the things we asked for, and their dependencies (transitively).
> Fully minimize and tidy up image layers
This only seems relevant for imperative images, built "from the inside" (e.g. the talk about contents being "overwritten"?)
> Splitting up processes and services for breaking up images
The helper-functions in Nixpkgs contain tricks to automatically figure out which content is better kept in separate layers, based on how many times they're referenced (as a proxy for how often they'll be shared across containers). Docker only supports 128 layers, so the remaining content gets combined into the final layer; e.g. see https://grahamc.com/blog/nix-and-layered-docker-images
Edit: Any special effort beyond using nix, I should say; that is a significant cost if you're not already paying it.
"Docker images" are .tar.gz files containing some binary executable.
The article starts by putting an entire Linux distribution into the .tar.gz (Ubuntu, Alpine, etc.), then goes through several ways to try and make the result smaller.
Nix does things the other way: the .tar.gz will only contain the specified binary, plus its dependencies. It's minimal by default (although there are still tricks to making things smaller, e.g. taking dependencies from `pkgsMusl` instead of `pkgs`, to avoid glibc)
[1] https://community.fly.io/t/running-reproducible-rust-a-fly-a...
https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-supp...
But for people who don't know nix its a bit strange to say that Dockerfiles are tedious, and then proceed to show a much more arcane and verbose way of building them as a solution to that tediousness.
The one thing that makes Dockerfiles attractive (and Docker in general), is that they are so simple and intuitive. You can immediately leverage your basic linux knowledge. That is why a lot of people like it whose main job is not about packaging.
Overtime, I had to go back to fix "working" base images that had become stale, and when dealing with layers and layers of base images this really gets tedious.
BTW, I haven't dealt with nix generated images in production, I'm not aware of possible drawbacks (other than people not being familiar with nix), so I'm not really endorsing it, although I would like to try.
What I found cumbersome though is to «cross-compile» images, I'm on mac/arm but the images need to run on linux/amd64. Ok my code is JavaScript (Node.js), I can compile the sources into plain JS on any machine, and then pack into an image together with nodejs from `pkgsLinux = import <nixpkgs> { system = "x86_64-linux"; };`, and all will work. I can even install node_modules on my build machine since the node_modules folder is (often) just a bunch of .js files. But some dependencies have native code, so I'd have to cross-compile those. And that's where I gave up.
I push OCI images to AWS ECR, so it would probably work.
> I'm on mac/arm but the images need to run on linux/amd64
I've never used the cross-compilation support in Nixpkgs. My work machine runs macOS (x86_64), so I have a Linux VM as a remote builder. I happen to use LimaVM ( https://github.com/lima-vm/lima ), but anything would work (Qemu, VirtualBox, the VM bundled with Docker Desktop, etc.).
PS: Since containers only work on Linux, and I'm often working on macOS, I use the following assertion to check that I'm not accidentally including Mac builds in a layer:
assert all (d: ((d.type or null) == "derivation") -> (d.system == "x86_64-linux")) drvs;Generally, getting a good understanding of Nix takes some time. It's hard because you need to understand what Nix does, and understand what you're trying to do, and how it constrains what you're trying to do.
Some techniques for smaller containers, such as squashing, actually makes downloads worse.
If you are running on your own (bare metal) Kubernetes or other orchestrators, rather go for smaller image sizes.
You can make use of this with multistage builds or with build systems like Bazel and Please.build.
[0] - https://github.com/GoogleContainerTools/distroless/blob/main...
Usually, images have steps to build (which requires source code, dependencies, etc), and the entry point at the end. Splitting the build- and run-time means you're shipping an image that only includes what's needed to run (not build) the application.
I find that it's easier to debug the build process with a "standard" distribution.
https://salsa.debian.org/gioele/debian-vm-minimal-pkgs
Various folks in Debian are working on shrinking the Essential package set:
https://wiki.debian.org/Proposals/EssentialOnDiet
mmdebstrap offers the ability to create Debian installs that are less than the Essential package set:
This surfaces when adding larger files.
Similarly, don’t wget in a run command, then chmod. Use add with a url where possible, combine with chmod flag. When not easy (file from authentication protected storage), you can optionally use a named stage and copy from that stage into your target image. You can also copy from another image!
EDIT: I should add that I already have multi stage builds in place. My main dependencies are fastapi, gunicorn, redis, sqlalchemy, snowflake-sqlalchemy, snowflake-connector-python, fastapi-pagination, pymysql, fastapi-redis-cache, pandas. I'm willing to forego pandas if the size of the image reduces.
Developers joining the team now must understand nix, scripting and a lot of other things just to build a simple docker image.
Just stop.
Even if it generates a lot of really stupid test cases, including them in a separate package would be incredibly powerful for handling stupid mistakes.
You should optimise for performance and security. For example python's base image is slower, than a container built on ubuntu for python apps. This is shown by several tests. Pythons base image is built on Alpine, which has shown to also have longer build times, and obscure bugs.
So a smaller container image is not always better. Instead optimise for performance and security.
No one said it is _always_ better. Obviously you can make a smaller but worse image.
Making the image smaller, without affecting its performance or security, can reduce costs (storage + network) and can make deployments much faster (and so improve reliability, and Developer Experience).
When updating images on multiple devices on a 10 Mbit connection the difference between, say, a 500 MB image and a functionally equivalent 50 MB image can be quite significant.
The Python image on Docker Hub has Debian, Alpine, and Windows Server versions: https://hub.docker.com/_/python
The default (`python:latest` or `python:3.10`) is using Debian bullseye. You need to ask for Alpine by using a specific tag (`python:3.10-alpine`).