How to create a minimal docker image for a Ruby (Rails) app?

Hi there!

We’re currently investigating the usage of Nix for managing our dependencies, to make onboarding to our app for new developers easier.

Besides this, we’re looking to re-use much of the same for our deployment (for the reasons of having a single source of truth instead of having to maintain two disparate build procedures side-by-side).

Currently, deployment is Dockerfile-based (based on this template).
We tried using the dockerTools in a naive way:

dockerImage = pkgs.dockerTools.buildImage {
         name = "our-app";
         copyToRoot = pkgs.buildEnv {
           paths = [ rubyProdEnv ];
           pathsToLink = [ "/bin" ];
         };
         config = {
           WorkingDir = "/app";
           Volumes = {
             "/app" = {};
           };
           Cmd = [ "${rubyProdEnv.bundler}/bin/bundler exec puma -C config/puma.rb" ];
         };
       };

But this creates very large (2GB rather than < 200MB as with the current Dockerfile) images.
(And it might be wrong in some other ways as well).

Diving inside the resulting Docker image, we can see that:

  • All gems and their transitive dependencies are included, and not only those that are part of the ‘production’ ruby environment.
  • Any other build tools are included. The resulting image contains perl, python3, nodeJS, imagemagick, V8, ghostscript, etc.

So my question is twofold:

  • How to limit the gems which bundix loads to only the ones which are used in production?
  • How to strip away anything that is only used during ‘building’ (such as NodeJS which precompiles some JS modules to files that browsers can understand) from the final generated image?

Hi Marten!

Have you inspected the closure of rubyProdEnv using nix path-info -r and also nix why-depends?

(These commands require the experimental feature nix-command)

1 Like

All gems and their transitive dependencies are included, and not only those that are part of the ‘production’ ruby environment.

Normally, transitive dependencies are part of the production environment by definitions. If there are unnecessary ones because dependency A depends on dependency B for features in A which your app does not use, you often need to re-build dependency A without support B.
This mostly happens via flags e.g. in an overlay over nixpkgs. But depends on the details.

The commands @NobbZ suggested could help you to investigate why something depends on something else. I also like https://github.com/utdemir/nix-tree for that task. The second step is often to read the nix source for your dependencies. But I don’t use ruby personally

1 Like

@phaer I could have worded that better. The problem is not that transitive dependencies are included, but rather that also all ‘development’ and ‘test’ dependencies (and whatever they depend on) are included.

Thanks for mentioning nix-tree!