Secure and Optimize a Rails Web Application with Bitnami's Production Containers
If you're a Ruby on Rails developer looking to publish a Docker image of your Rails application, there are some basic requirements you're always going to want in your production container image:
- It needs to have the latest language runtime and bug fixes.
- It needs to be small in size and contain only the minimum necessary files.
- It needs to be secure.
Bitnami's production container for Ruby provides a good starting point. This Bitnami image contains the minimal dependencies required for a Ruby application to work. It doesn't include any development dependencies, so it's smaller and quicker to build. Plus, Bitnami container images are always secure, optimized and up-to-date, so you can rest assured that your application always has access to the latest language features and security fixes.
This guide walks you through the process of creating a minimal, secure and production-ready image of your Rails application using Bitnami's production container for Ruby and a multi-stage Docker build process.
Assumptions and prerequisites
This guide makes the following assumptions:
- You have a basic understanding of how containers work.
- You have a Docker environment installed and configured. Learn more about installing Docker.
Step 1: Obtain the application source code
If you already have a Rails application of your own, you can use that instead and skip to Step 2.
This guide uses an example Rails application which only generates a welcome message.
- Begin by generating a skeleton Rails application. If you don't have Rails installed, an easy way to do this is to use Docker to execute the Rails CLI from Bitnami's Rails image. This allows you to execute Rails commands using the Bitnami Rails container on your host, exactly as though you had Rails already installed. Use the following Docker command:
docker run --rm -v $(pwd):/app bitnami/rails:latest rails new myapp --skip-bundle --skip-active-record
The -v argument tells Docker to mount the host's current directory into the container's /app path, so that the effect of the Rails CLI command is seen on the host. The --rm parameter tells Docker to automatically clean up by deleting the container once it's completed executing the command.
- Change to the application directory created by the previous command and create a controller and view, again using the Rails CLI from Bitnami's Rails image:
cd myapp docker run --rm -v $(pwd):/app bitnami/rails:latest rails generate controller Welcome index
- Update the generated default view with a custom message:
echo '<h1>Hello from Bitnami!</h1>' > app/views/welcome/index.html.erb
Step 2: Generate the Dockerfile
Create the following Dockerfile in your application directory:
# first stage FROM bitnami/rails:6 as builder USER root ENV RAILS_ENV="production" COPY . /app WORKDIR /app RUN bundle config set deployment 'true' && \ bundle config set without 'development test' RUN bundle install && \ bundle exec rake assets:precompile # second stage FROM bitnami/ruby:2.6.5-prod as prod COPY --from=builder /app/ /app/ RUN useradd -r -u 1001 -g root nonroot RUN chown -R nonroot /app USER nonroot WORKDIR /app EXPOSE 3000 ENV RAILS_ENV="production" CMD ["bin/rails", "server"]
This Dockerfile consists of two build stages:
- The first stage uses the Bitnami Rails development image to copy the application source from the current directory and install the required application modules using Bundler.
- The second stage uses the Bitnami Ruby production image to create a minimal image that only contains Ruby, Node.js, the application source code and related modules.
Bitnami's Ruby production image is different from its Ruby development image, because the production image (tagged with the suffix prod) is based on minideb and does not include additional development dependencies. It is therefore lighter and smaller in size than the development image and is commonly used in multi-stage builds as the final target image.
Let's take a closer look at each of these stages.
First build stage
- The Dockerfile begins by specifying the base image to use - in this case, Bitnami's Rails development image, which also includes Node.js.
- It copies the application source code and installs the required gems using Bundler, which is included in the Bitnami Rails image.
- It pre-compiles the assets with Rake.
If you are using a different Rails application, you may need to perform additional steps here to prepare your application for deployment.
The output of this first stage is a development image containing the application and all its dependencies.
Second build stage
- The second stage uses Bitnami's Ruby v2.6.5 production image, which contains only the minimum necessary dependencies for a Ruby application.
- It copies the application source and the installed gems from the previous stage.
- It creates a non-root user account that the application will run under. For security reasons, it's recommended to always run your application using a non-root user account.
- It sets the RAILS_ENV environment variable so that the Rails server starts in production mode.
- It exposes the container port 3000 and starts the Rails server.
The output of this second stage is a minimal production-ready image containing the Ruby runtime, the application and its dependencies.
Step 3: Build and test the production image
Once the Dockerfile is created, execute the command below in the application directory:
docker build -t hello .
Run the image using the command below and bind the server to port 3000 of the Docker host
docker run -p 3000:3000 hello
You should see the output below as the Rails server starts:
If you open a browser and navigate to http://IP-ADDRESS:3000/welcome/index, replacing the IP-ADDRESS placeholder with the IP address of the Docker host, you should see the output of the Rails application, as shown below:
This multi-stage build approach with a non-root account and a production Bitnami image is recommended to create a secure and minimalist Docker image of your application for production environments.