Getting Started with the Ruby Chainguard Image

Tutorial on how to get started with the Ruby Chainguard Image

The Ruby images maintained by Chainguard are a mix of development and production distroless images that are suitable for building and running Ruby workloads.

Because Ruby applications typically require the installation of third-party dependencies via Rubygems, using a pure distroless image for building your application would not work. In cases like this, you’ll need to implement a multi-stage Docker build that uses one of the -dev images to set up the application.

In this guide, we’ll build two example applications that demonstrate how to use Ruby container images based on Wolfi as a runtime. In the first, we’ll use a minimal image containing just Ruby to execute a demo that doesn’t have any external dependencies. In the second example, we’ll set up a multi-stage Docker build to run a demo that requires the installation of Rubygems via bundler.

What is distroless Distroless images are minimalist container images containing only essential software required to build or execute an application. That means no package manager, no shell, and no bloat from software that only makes sense on bare metal servers.
What is Wolfi Wolfi is a community Linux undistro created specifically for containers. This brings distroless to a new level, including additional features targeted at securing the software supply chain of your application environment: comprehensive SBOMs, signatures, daily updates, and timely CVE fixes.
Chainguard Images Chainguard Images are a mix of distroless and development images based on Wolfi. Nightly builds make sure images are up-to-date with the latest package versions and patches from upstream Wolfi.

Example 1: Minimal Ruby Image in Single Stage Build

We’ll start by creating a small command-line Ruby application to serve as a demo. This application has no external dependencies; it will read from a text file containing facts about octopuses, and output a random line from that file. This demo is also available in our demos repository, if you want to review the source files before building it.

Step 1: Setting up the Application

First, create a directory and then move into it for your app. Here we’ll use octo-facts:

mkdir ~/octo-facts && cd ~/octo-facts

Next, create a new file to serve as the application entry point. Here, we’ll use octo.rb. You can edit this file in whatever code editor you would like. We’ll use nano as an example.

nano octo.rb

The following Ruby code will read a random line from the facts.txt file and print it out to the terminal:

#!/usr/bin/env ruby

class OctoFact
    attr_accessor :source

    def initialize(source = "facts.txt")
        @source = source
    end

    def random_line
        puts File.readlines(@source).sample
    end
end

if __FILE__ == $0
    fact = OctoFact.new
    fact.random_line
end

Copy this code to your octo.rb script, then save and close the file.

Next, pull down the facts.txt file with curl. You can inspect the file’s contents before downloading it to ensure it is safe to do so. Make sure you are still in the same directory where your octo.rb script is.

curl -O https://raw.githubusercontent.com/chainguard-dev/edu-images-demos/main/ruby/octo-facts/facts.txt

With all files in place, you can now run the demo using Docker. The following command will execute the demo code using the same base image we’ll use to build our Dockerfile in the next step. It will set up a volume sharing the files in the current directory with the location /work inside the container, then execute the octo.rb script:

docker run --rm -v ${PWD}:/work cgr.dev/chainguard/ruby octo.rb

And you should get output like this, with a random fact about octopuses:

Octopuses have decentralized brains.

In the next step, we’ll create a Dockerfile to build and run the demo.

Step 2: Setting up the Dockerfile

With the demo ready, you can now set up a Dockerfile to build a custom image for your Ruby application. Make sure you’re still on the same directory as the application files, then create a new Dockerfile using your text editor of choice:

nano Dockerfile

The following Dockerfile will:

  1. Start a new image based on the cgr.dev/chainguard/ruby:latest image;
  2. Set up a workdir at /app;
  3. Copy the application files to the workdir;
  4. Set up the entry point for the image as ruby octo.rb.

Copy the following content to your Dockerfile:

FROM cgr.dev/chainguard/ruby:latest

WORKDIR /app

COPY octo.rb facts.txt ./

ENTRYPOINT [ "ruby", "octo.rb" ]

Save and close the file when you’re done. Next, build the image with:

docker build . -t octo-ruby-demo

Once the build is finished, you can execute the image with:

docker run --rm octo-ruby-demo

And you should get output similar to what you got before, with a random octopus fact.

Example 2: Multi-Stage Build for Ruby Application Runtime

To demonstrate how to containerize a more complex application that requires the installation of third party dependencies, we’ll create a second demo that uses a Docker multi-stage build, which will combine the cgr.dev/chainguard/ruby:latest-dev development image to build the application and the cgr.dev/chainguard/ruby:latest distroless image to run it.

This demo will use the rainbow Ruby gem to output to the command line interface a colorful quote, inspired by cowsay.

Step 1: Setting up the Application

First, create a directory for your app. Here we’ll use inky-says:

mkdir ~/inky-says && cd ~/inky-says

Then, set up your Gemfile:

nano Gemfile

Copy the following content into your Gemfile to require the Rainbow Gem:

source 'https://rubygems.org'

gem 'rainbow'

Save and close the file.

Next, create a new Ruby script file called inky.rb:

nano inky.rb

The following code outputs a colorful quote provided at runtime, incorporating an ASCII representation of Inky that is pulled from an inky.txt file located at the same directory as the ruby script. The printed quote colors alternate randomly between purple and magenta.

#!/usr/bin/env ruby

require 'rainbow'
Rainbow.enabled = true

class Inky
    def says(message = "Hello World")
        colors = [:purple, :magenta]
        words = message.split(" ")

        print "\n ".ljust(40, " ")
        words.each do |n|
            print Rainbow(n).color(colors.sample) + " "
        end

        print "\n"
        puts File.readlines('inky.txt')
    end
end

if __FILE__ == $0
    inky = Inky.new
    inputArray = ARGV
    message = inputArray.length > 0 ? inputArray.join(' ') : "Hello Wolfi"
    inky.says(message)
end

Copy this code to your inky.rb script, then save and close the file.

Next, pull down the ASCII inky.txt file with curl. You can inspect the file contents before downloading it to ensure it is safe to do so. Make sure you are still in the same directory where your inky.rb script is.

curl -O https://raw.githubusercontent.com/chainguard-dev/edu-images-demos/main/ruby/inky-says/inky.txt

With everything in place, you can now work on the Dockerfile that will install the application dependencies and execute your Ruby script.

Step 2: Setting Up the Dockerfile

To make sure our final image is distroless while still being able to install Rubygems, our build will consist of two stages: first, we’ll build the application using the dev image variant, a Wolfi-based image that includes the Gem executable, Bundler, and other useful tools for development. Then, we’ll create a separate stage for the final image. The resulting image will be based on the distroless Ruby Wolfi image, which means it doesn’t come with the Gem executable or even a shell.

Create a new Dockerfile using your code editor of choice, for example nano:

nano Dockerfile

The following Dockerfile will:

  1. Start a new build stage based on the cgr.dev/chainguard/ruby:latest-dev image and call it builder;
  2. Set up environment variables that define the default location of installed Gems;
  3. Copy the Gemfile from the current directory to the /work location in the container;
  4. Install Bundler and run bundle install;
  5. Start a new build stage based on the cgr.dev/chainguard/ruby:latest image;
  6. Set up environment variables that define the default location of installed Gems;
  7. Copy build artifacts from builder and into the final image
  8. Copy the inky.rb and inky.txt files into the final image
  9. Set up the application entry point as ruby inky.rb.

Copy this content to your own Dockerfile:

FROM cgr.dev/chainguard/ruby:latest-dev as builder

ENV GEM_HOME=/work/vendor
ENV GEM_PATH=${GEM_PATH}:/work/vendor
COPY Gemfile /work/
RUN gem install bundler && bundle install

FROM cgr.dev/chainguard/ruby:latest

ENV GEM_HOME=/work/vendor
ENV GEM_PATH=${GEM_PATH}:/work/vendor

COPY --from=builder /work/ /work/
COPY inky.rb inky.txt /work/

ENTRYPOINT [ "ruby", "inky.rb" ]

Save the file when you’re finished.

You can now build the image with:

docker build . -t inky-says

Once the build is finished, run the image with:

docker run --rm inky-says Wolfi says hi

And you should get output like this:


                                       Wolfi says hi
                   @@@
              @@@*******@@@              /
           @@%**@@@@*******@@@          /
          @@*@@***********///@@        /
         @*************///////&@      /
        @@*****%@@@**////@@@&//@@    /
        @*****@**##@////@//##@//@   /
        @@*****@@@@//////@@@@//@@  /
      ,@@@@***////////////////@@@@,
 @@@*******/////////////////////(((((@@@
 @*******//////////////////////((((((((@
    @@@/////////@///////@///(((((%@@@
     #@///////@@@///////@@@((((((#@
         @@@    /@@///@@,    @@@

If you inspect the image with a docker image inspect inky-says, you’ll notice that it has only three layers, thanks to the use of a multi-stage Docker build.

docker image inspect inky-says
...
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:ec653fe8da922557dd1d78b47b2c0074a6e9257e5a15e596bc4e1fb1e325c3d8",
                "sha256:d9541a2e82274d8140ed7f4cc79ce92f295d13951585c4a433f1baa35015e1eb",
                "sha256:a44e5fee28e11702a63418bf76c009b1b4948f1b0426e0f199b83702ae187796"
            ]
        },
        "Metadata": {
            "LastTagTime": "2023-05-09T19:53:16.060125796+02:00"
        }
    }
]

In such cases, the last FROM section from the Dockerfile is the one that composes the final image. That’s why in our case it only adds two layers on top of the base cgr.dev/chainguard/ruby:latest image, containing the two COPY commands we use to copy the application files and its dependencies to the final image.

It’s worth highlighting that no code or data is carried from one stage to the other unless you use a COPY command to explicitly copy it. This approach facilitates creating a slim final image with only what’s absolutely necessary to execute the application. Using a multi-stage build like this, without shell tools and interactive language interpreters built in also makes your final image more secure.

Advanced Usage

If your project requires a more specific set of packages that aren't included within the general-purpose Ruby Chainguard Image, you'll first need to check if the package you want is already available on the wolfi-os repository.

Note: If you're building on top of an image other than the wolfi-base image, the image will run as a non-root user. Because of this, if you need to install packages with apk install you need to use the USER root directive.

If the package is available, you can use the wolfi-base image in a Dockerfile and install what you need with apk, then use the resulting image as base for your app. Check the "Using the wolfi-base Image" section of our images quickstart guide for more information.

If the packages you need are not available, you can build your own apks using melange. Please refer to this guide for more information.

Last updated: 2023-05-10 11:07