Rails 6 production container with Docker

Joël AZÉMAR
6 min readMar 19, 2021

--

Without docker-compose!

Disclaimers: This is not an article about how Dockerize a Rails App for development, but how to Dockerize an app in a production environment ready to deploy. Ideally, this part should be delegated to the CI.

There are plenty of examples of how Dockerize an App in development mode and often with docker-compose. That is great but it doesn’t help when it comes to host and deploy our containers in production.

Let’s start with the simple weblog of rails [1] btw, the code can be found here [2] This article might take some short cuts to keeping it short and straight to the point.

If Ruby, Rails and Postgresql are already installed on your machine you might skip the next section, otherwise, check it out.

Rails with Docker

The official Rails image on the Docker hub is deprecated [3] for good reason, it doesn’t make sense to have a universal image for Rails as we are likely to customize it for our needs. Here a simple image to create a Rails app with Postgresql.

The Rails Dockerfile

Note: If you get an error with mimemagic, just add shared-mime-info in the apk list of dependencies.

Now we can build the Rails Docker image:

docker build \
--build-arg BUNDLER_VERSION=2.2.14 \
--build-arg RAILS_VERSION=6.1.3 \
--build-arg APP_PATH=/work \
--tag joel/rails:3.0.0 \
.

We can see the newest image create on our system

docker image ls                                                                                                                             REPOSITORY TAG    IMAGE ID       CREATED          SIZE
joel/rails 3.0.0 93e8912fa836 24 minutes ago 649MB

Tips: If you run into problems during the build of the image and you want to debug, you can get more output with those 2 extra instructions:

docker build \
--build-arg BUNDLER_VERSION=2.2.14 \
--build-arg RAILS_VERSION=6.1.3 \
--build-arg APP_PATH=/work \
--progress=plain \
--no-cache \
--tag joel/rails:3.0.0 \
.

Create a Rails App

Now we can create the new Rails App.

docker run --rm \
--mount type=bind,source=$PWD,target=/work \
joel/rails:3.0.0 \
rails _6.1.3_ new weblog --database=postgresql

Note: we bind the current directory onto our container, like that the code generated will be reflected in our file system and we can open the code with our text editor and change what needs to be modified.

For conviniency we create an alias for this command

alias r="docker run --rm --mount type=bind,source=$PWD,target=/work joel/rails:3.0.0"

Starting Postgresql

We are using the Docker Postgresql Image.

First, let create a dedicated network for our application

docker network create weblog-bridge-docker-network

We can find it here:

docker network ls                                                                                                                          NETWORK ID     NAME                            DRIVER    SCOPE
f3062d22ce7a weblog-bridge-docker-network bridge local

This way we ensure all communications are going through this network.

Before starting the database we create a volume for our data:

docker volume create weblog-db-data

Note: The postgres image create a default volume for us but with a random id as name. It’s nicer to have proper name:

docker volume ls                                                                                                                           DRIVER    VOLUME NAME
local weblog-db-data

Now we can start our database:

docker run --rm --detach \
--name weblog-db \
--env POSTGRES_PASSWORD=postgres \
--env POSTGRES_USER=postgres \
--network weblog-bridge-docker-network \
--mount source=weblog-db-data,target=/var/lib/postgresql/data \
postgres:13.2-alpine

Note: Do not use underscore on the name of that container, Docker use the name of the container to resolve hostname internally, see the container names as hostnames. If Docker is flexible when resolving hostnames, ActiveRecord is less, it uses URI::RFC2396_Parser to parse the DATABASE_URL so we need a valid hostname format.

An important thing here is to start the container in the same network, this way the Rails app can communicate freely with the database.

Start Redis

Before running our Rails app we need to start a Redis instance as ActionCable use it in production. If we follow along with the DHH weblog example we are going to need it.

Again here we start to create a named volume:

docker volume create weblog-redis-data

And start Redis

docker run --rm --detach \
--name weblog-redis \
--network weblog-bridge-docker-network \
--mount source=weblog-redis-data,target=/data \
redis redis-server --save 60 1 --loglevel warning

An important thing here is to start the container in the same network, this way the Rails app can communicate freely with Redis.

btw we might need to add the redis gem:

group :production do
gem "hiredis"
end

be sure to install it r bunde

Create a Resources

Let’s create something on our app.

r rails generate scaffold Post title:string body:text

Lovely, now we have something to play with.

Dockerize the Rails App

We are ready to Dockerize our Rails app

Note: We use Docker multi-stage in order to reduce the weight of the image, we are down from +1Gb to 0.4Gb. If you don’t feel confident with that just remove the code from the second FROM

Now we have to build the image:

docker build --squash \
--tag joel/weblog:latest \
.

Note: I use --squash to reduce the number of Docker layers. This is part of the last and experimental buildx if you are not using it, just omit this option.

FYI I’m using Docker Engine 20.10.3 with these settings:

cat ~/.docker/daemon.json | jq                                                                                                             
{
"debug": true,
"experimental": true,
"features": {
"buildkit": true
}
}

We can check the image size

docker images joel/weblog:latest --format "{{.Repository}}:{{.Tag}} {{.Size}}"

Create the Rails Image

Time to create the Docker image of our Rails App.

docker build --squash \
--tag joel/weblog:latest \
.

Starting the Rails App in Production Mode

Now everything is set up we can start our app.

Things are getting interesting

docker run --rm \
--name weblog-prod-app \
--env RAILS_LOG_TO_STDOUT=true \
--env RAILS_MAX_THREADS=8 \
--env RAILS_MIN_THREADS=1 \
--env WEB_CONCURRENCY=1 \
--env REDIS_URL=redis://weblog-redis:6379/1 \
--env DATABASE_URL="postgres://postgres:postgres@weblog-db:5432/weblog_db_prod?pool=5" \
--network weblog-bridge-docker-network \
--publish 3025:3000 \
-it joel/weblog:latest sh

What have we just done here? We’ve started the container with all the configurations we wanted to.

We’ve indicated where to find Redis and Postgres, note how the hostnames are replaced by the container names. As well we didn’t publish any ports as the containers have full access to their network.

We can check if all our containers are in the same network with:

docker network inspect weblog-bridge-docker-network

Now we can interact with the container to create the database and start the app.

docker exec weblog-prod-app sh -c 'rails db:setup'

Now we can start the app

docker exec weblog-prod-app \
sh -c 'rails server -p 3000 --early-hints -b 0.0.0.0'

Note: Here, we reuse the running container with`exec`, but we’re likely to need to run the complete command next time:

docker run --rm \
--name weblog-prod-app \
--env RAILS_LOG_TO_STDOUT=true \
--env RAILS_MAX_THREADS=8 \
--env RAILS_MIN_THREADS=1 \
--env WEB_CONCURRENCY=1 \
--env REDIS_URL=redis://weblog-redis:6379/1 \
--env DATABASE_URL="postgres://postgres:postgres@weblog-db:5432/weblog_db_prod?pool=5" \
--network weblog-bridge-docker-network \
--publish 3025:3000 \
joel/weblog:latest rails server -p 3000 --early-hints -b 0.0.0.0

You can go visit http://localhost:3025/posts

Or curl the app.

Important note, this container is not connected with your filesystem, any changes made on the app need to recreate the image to reflect them.

That it! Hope it demystifies how to start a Rails App in production mode with Docker. As we are not using docker-compose we can use the Orchestre we want to and manage our containers as we wish.

--

--