How to Dockerize a Sinatra application

by Jason Swett,

Why we’re doing this

Docker is difficult

In my experience, Dockerizing a Rails application for the first time is pretty hard. Actually, doing anything with Docker seems pretty hard. The documentation isn’t that good. Clear examples are hard to find.

Dockerizing Rails is too ambitious as a first goal

Whenever I do anything for the first time, I want to do the simplest, easiest possible version of that thing before I try anything more complicated. I also never want to try to learn more than one thing at once.

If I try to Dockerize a Rails application without any prior Docker experience, then I’m trying to learn the particulars of Dockerizing a Rails application while also learning the general principles of Docker at the same time. This isn’t a great way to go.

Dockerizing a Sinatra application gives us practice

Dockerizing a Sinatra application lets us learn some of the principles of Docker, and lets us get a small Docker win under our belt, without having to confront all the complications of Dockerizing a Rails application. (Sinatra is a very simple Ruby web application framework.)

After we Dockerize our Sinatra application we’ll have a little more confidence and a little more understanding than we did before. This confidence and understanding will be useful when we go to try to Dockerize a Rails application (which will be a future post).

By the way, if you’ve never worked with Sinatra before, don’t worry. No prior Sinatra experience is necessary.

What we’re going to do

Here’s what we’re going to do:

  1. Create a Sinatra application
  2. Run the Sinatra application to make sure it works
  3. Dockerize the Sinatra application
  4. Run the Sinatra application using Docker
  5. Shotgun a beer in celebration (optional)

I’m assuming you’re on a Mac and that you already have Docker installed. If you don’t want to copy/paste everything, I have a repo of all the files here.

(Side note: I must give credit to Marko Anastasov’s Dockerize a Sinatra Microservice post, from which this post draws heavily.)

Let’s get started.

Creating the Sinatra application

Our Sinatra “application” will have just one file. The application will have just one endpoint. Create a file called hello.rb with the following content.

# hello.rb

require 'sinatra'

get '/' do
  'It works!'
end

We’ll also need to create a Gemfile that says Sinatra is a dependency.

# Gemfile

source 'https://rubygems.org'

gem 'sinatra'

Lastly for the Sinatra application, we’ll need to add the rackup file, config.ru.

# config.ru

require './hello'

run Sinatra::Application

After we run bundle install to install the Sinatra gem, we can run the Sinatra application by running ruby hello.rb.

$ bundle install
$ ruby hello.rb

Sinatra apps run on port 4567 by default, so let’s open up http://localhost:4567 in a browser.

$ open http://localhost:4567

If everything works properly, you should see the following.

Dockerizing the Sinatra application

Dockerizing the Sinatra application will involve two steps. First, we’ll create a Dockerfile will tells Docker how to package up the application. Next we’ll use our Dockerfile to build a Docker image of our Sinatra application.

Creating the Dockerfile

Here’s what our Dockerfile looks like. You can put this file right at the root of the project alongside the Sinatra application files.

# Dockerfile

FROM ruby:2.7.4

WORKDIR /code
COPY . /code
RUN bundle install

EXPOSE 4567

CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]

Since it might not be clear what each part of this file does, here’s an annotated version.

# Dockerfile

# Include the Ruby base image (https://hub.docker.com/_/ruby)
# in the image for this application, version 2.7.4.
FROM ruby:2.7.4

# Put all this application's files in a directory called /code.
# This directory name is arbitrary and could be anything.
WORKDIR /code
COPY . /code

# Run this command. RUN can be used to run anything. In our
# case we're using it to install our dependencies.
RUN bundle install

# Tell Docker to listen on port 4567.
EXPOSE 4567

# Tell Docker that when we run "docker run", we want it to
# run the following command:
# $ bundle exec rackup --host 0.0.0.0 -p 4567.
CMD ["bundle", "exec", "rackup", "--host", "0.0.0.0", "-p", "4567"]

Building the Docker image

All we need to do to build the Docker image is to run the following command.

I’m choosing to tag this image as hello, although that’s an arbitrary choice that doesn’t connect with anything inside our Sinatra application. We could have tagged it with anything.

The . part of the command tells docker build that we’re targeting the current directory. In order to work, this command needs to be run at the project root.

$ docker build --tag hello .

Once the docker build command successfully completes, you should be able to run docker images and see the hello image listed.

Running the Docker image

To run the Docker image, we’ll run docker run. The -p 4567:4567 portion says “take whatever’s on port 4567 on the container and expose it on port 4567 on the host machine”.

$ docker run -p 4567:4567 hello

If we visit http://localhost:4567, we should see the Sinatra application being served.

$ open http://localhost:4567

Conclusion

Congratulations. You now have a Dockerized Ruby application!

With this experience behind you, you’ll be better equipped to Dockerize a Rails application the next time you try to take on that task.

10 thoughts on “How to Dockerize a Sinatra application

  1. Paul Mitchell

    Hi,

    We do a couple of things differently. However, I think the most interesting is that we use the ENTRYPOINT directive in the Dockerfile to do bundle exec and then run the rails app on the CMD directive.

    This allows us to run the rails console inside the running container like “docker exec -it rails-app rails console”. So this would include any rails or rake task.

    Reply
  2. Ben

    Jason,

    It’d be interesting to hear your thoughts as to the “why” it’s considered good or necessary to Dockerizing these web applications. What do we gain from the process? Operation/Administration, Monitoring, Deployment, etc..

    Truly curious! I’ve been through the process a number of times just to get familiarity with containerization but I still don’t see the pros.

    Thanks!

    Reply
    1. Jason Swett Post author

      That’s a really good question. It has taken me a while to come to an understanding myself.

      For me it’s two things. First, we recently hired a developer where I work and we’re planning to hire more. The process of getting our developer set up with our application was a fairly painful and manual process. It would be nice if future developers could just download and run a Docker image instead.

      Second, I expect that it might make deployment and operations easier. Rather than having an Ansible playbook that configures each server the way it needs to be, I can just have a Docker image that gets run in the cloud instead. (Tools like Ansible may well still have their place in our infrastructure because Docker of course isn’t meant to do everything.)

      So with those two things in mind, we’d go from two different ways of getting the app set up (one for dev environments and one for prod) we’d have just one, which could be simpler.

      Reply
  3. Jochem

    “Docker can apparently only expose port 80.”

    Don’t think that is true, but perhaps on your machine traffic to other ports is blocked?

    After
    $ docker run -p 8080:4567 hello
    your container should respond to requests at
    http://localhost:8080
    just fine.

    Reply
  4. Steffen

    I second Jochen’s contribution :-).

    If you want to access your app on port 80 you just have to start

    docker run hello -p 4567:80

    thank you for the article!

    Steffen

    Reply
  5. David Salgado

    Nice article.

    I’d strongly recommend breaking out the copy command so that you first copy the Gemfile & Gemfile.lock, then do bundle install, and then copy the rest of your application code separately, like this: https://github.com/ministryofjustice/cloud-platform-helloworld-ruby-app/blob/main/Dockerfile

    If you do it all in one, i.e. “COPY . /code”, then whenever you run `docker build` you’ll be reinstalling all your gems, even if nothing changed, because *any* change in *any* of your code files will cause that step to be repeated.

    If you do it separately, like the example I linked to, then you only reinstall your gems if your Gemfile changes, not on every code change. That will result in *much* faster docker builds as you iterate your code. The general rule is, put the stuff that changes often near the bottom of your Dockerfile.

    Reply
  6. Black Onyx

    To me it looks like the bundler approach duplicates the isolation efforts of the libs? Didn’t we already gain isolation by using a docker container? Why not just installing the required libs directly inside the Dockerfile?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *