Rails 6 production container with Docker
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.
References:
[1] Rails 5: The Tour https://www.youtube.com/watch?v=OaDhY_y8WTo
[2] Weblog https://github.com/joel/dweblog
[3] Rails Hub Docker https://hub.docker.com/_/rails