DEV Community

Alain Mauri
Alain Mauri

Posted on • Updated on

Dockerfile template for Rails Apps

Disclaimer:

This post assumes that you know how Docker works and you know how to build images passing some arguments and that you know how to run a container.
It's not and introduction to Docker.
If you find any kind of error please contact me and I will amend the post.

Update:

I've created a Github repo with some other stuff to support this post.

A Dockerfile template for Rails apps

Problem:

I wanted to find a way to improve the on-boarding process of new developers joining my team and to improve the day to day life of my team's "legacy" developers. At that time, everyone had to manage different Ruby versions for different projects, manage different environment variables and so on. You can imagine how painful could have been such a process for a new developer in the team so, I started looking around for possible solutions.

Solution:

After searching and reading a lot around, I came up with the conclusion that Docker could have been a good solution.
Containers run in isolation, you can replicate a production environment, it runs on different systems and you can easily tweak it to test different features of your app.
So everything seemed easy and pretty much working out of the box so, I came up with this:

FROM ruby:2.5.3

# The qq is for silent output in the console
RUN apt-get update -qq && \
  apt-get install -y build-essential openssl libssl-dev nodejs-legacy less vim libsasl2-dev


# Sets the path where the app is going to be installed
# you can modify this as you wish
ENV WORK_ROOT /var
ENV RAILS_ROOT $WORK_ROOT/www/
ENV LANG C.UTF-8
ENV GEM_HOME $WORK_ROOT/bundle
ENV BUNDLE_BIN $GEM_HOME/gems/bin
ENV PATH $GEM_HOME/bin:$BUNDLE_BIN:$PATH

RUN gem install bundler -v 1.17.3

# Creates the directory and all the parents (if they don't exist)
RUN mkdir -p $RAILS_ROOT

WORKDIR $RAILS_ROOT

# We are copying the Gemfile first, so we can install
# all the dependencies without any issues
# Rails will be installed once you load it from the Gemfile
# This will also ensure that gems are cached and only updated when
# they change.
COPY Gemfile ./

# Installs the Gem File.
RUN bundle install

# We copy all the files from the current directory to our
# application directory
COPY . $RAILS_ROOT

This was working fine for a while until one day... One day I decided that, because of the fact our production environment were Linux based, I wanted to go back to Linux and so, after giving back my Macbook Pro, I asked the company to provide me a Linux based machine.

After setting it up properly, the easiest part should have been checking out the projects, starting up a container and done! Ready to go!

No way!

There was one point that I did not consider at that time. Macbooks are nice and shiny machines, but Docker runs on them in a virtual machine while in a Linux environment it runs as a daemon with root permissions...

So the point is that, after running the container, all my files were "property" of the root user and I had to manually chmod then everytime.

This was annoying and not the purpose of the entire effort. Anyway this problem can be easily overcome by passing some environment variables while building the image and creating a user that has the same gid and uid of the host user:

ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION

ARG BUNDLER_VERSION
ARG uid
ARG gid
# The qq is for silent output in the console
RUN apt-get update -qq && \
  apt-get install -y build-essential openssl libssl-dev nodejs-legacy less vim libsasl2-dev

# To not run the container as root I create a users that shares
# uid and gid with the host user.
# In this way we can avoid having our working folder owned by root
RUN groupadd -g $gid webuser
RUN useradd -ms /bin/bash -u $uid -g $gid webuser

# Sets the path where the app is going to be installed
ENV WORK_ROOT /var
ENV RAILS_ROOT $WORK_ROOT/www/
ENV LANG C.UTF-8
ENV GEM_HOME $WORK_ROOT/bundle
ENV BUNDLE_BIN $GEM_HOME/gems/bin
ENV PATH $GEM_HOME/bin:$BUNDLE_BIN:$PATH

RUN gem install bundler -v $BUNDLER_VERSION

# Creates the directory and all the parents (if they don't exist)
RUN mkdir -p $RAILS_ROOT

RUN chown -R webuser:webuser $GEM_HOME
RUN bundle config --path=$GEM_HOME

WORKDIR $RAILS_ROOT

# We are copying the Gemfile first, so we can install
# all the dependencies without any issues
# Rails will be installed once you load it from the Gemfile
# This will also ensure that gems are cached and only updated when
# they change.
COPY Gemfile ./
COPY Gemfile.lock ./

RUN chown -R webuser:webuser $RAILS_ROOT

USER webuser
# Installs the Gem File.
RUN bundle install

# We copy all the files from the current directory to our
# application directory
COPY . $RAILS_ROOT

after doing it, I came up with a Dockerfile working on both Linux and MacOS machines (sorry Windows users I don't know if this works on your system).

I also added some other environment variables like Ruby version, Bundler version that can be passed to Docker while building the image.

You could think we're at the end, not at all. The company I'm working for uses LDAP so, uids and gids are big numbers and this carries a problem while creating the user in Docker I wasn't aware of. Basically if you add a user without --no-log-init option, Docker will try to create lastlog as a massive sparse file that could easily eating your disk space.
(more here and here)

So I ended up with this:

ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION

ARG BUNDLER_VERSION
ARG uid
ARG gid
# The qq is for silent output in the console
RUN apt-get update -qq && \
  apt-get install -y build-essential openssl libssl-dev nodejs-legacy less vim libsasl2-dev

# To not run the container as root I create a users that shares
# uid and gid with the host user.
# In this way we can avoid having our working folder owned by root
RUN groupadd -g $gid webuser
RUN useradd --no-log-init -ms /bin/bash -u $uid -g $gid webuser

# Sets the path where the app is going to be installed
ENV WORK_ROOT /var
ENV RAILS_ROOT $WORK_ROOT/www/
ENV LANG C.UTF-8
ENV GEM_HOME $WORK_ROOT/bundle
ENV BUNDLE_BIN $GEM_HOME/gems/bin
ENV PATH $GEM_HOME/bin:$BUNDLE_BIN:$PATH

RUN gem install bundler -v $BUNDLER_VERSION

# Creates the directory and all the parents (if they don't exist)
RUN mkdir -p $RAILS_ROOT

RUN chown -R webuser:webuser $GEM_HOME
RUN bundle config --path=$GEM_HOME

WORKDIR $RAILS_ROOT

# We are copying the Gemfile first, so we can install
# all the dependencies without any issues
# Rails will be installed once you load it from the Gemfile
# This will also ensure that gems are cached and only updated when
# they change.
COPY Gemfile ./
COPY Gemfile.lock ./

RUN chown -R webuser:webuser $RAILS_ROOT

USER webuser
# Installs the Gem File.
RUN bundle install

# We copy all the files from the current directory to our
# application directory
COPY . $RAILS_ROOT

Which is what I'm currently using.

Enjoy!

Top comments (0)