Last episode was about motivation and benefits of using docker for development. In this episode we’ll be dockerizing a Ruby on Rails app.
Before we start:
- It’s important you’re familiar with docker (as mentioned in the last episode) and docker-compose.
- We know there are tons of “Dockerizing X” tutorials/blog posts out there. It’s not the goal of this post to present you the absolute tutorial/blog post, but to show you the problems we encountered while taking our first steps using docker, and how to solve them (as those aren’t easily googleable).
Step 1: creating a Dockerfile
As you know, the first step is to create a Dockerfile in the root directory of our Rails app. The Dockerfile explains how the app is built, and how is it going to be executed. For a standard Rails app we normaly have (more or less) this
FROM ruby:2.6 RUN apt-get update -qq && apt-get install -y build-essential curl git libpq-dev nodejs WORKDIR /app COPY . /app COPY bin/entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh RUN bundle install ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 CMD ["rails", "server", "-b", "0.0.0.0"]
If you’re familiar with docker (and we encouraged that since the beginning of the post!) you’ll understand all of it. There’s nothing out of the ordinary in this
Dockerfile, but there’s one thing that you wouldn’t find in a regular rails project and that’s the entrypoint. The entrypoint is the image’s main command, and is often used in combination with helper scripts.
In our case, for a Rails app, a normal
bin/entrypoint.sh file would look like this:
#!/bin/sh set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /app/tmp/pids/server.pid exec "$@"
Let’s the start from the end of the script.
$@ means “all of the parameters passed to the script”. If we look at the
Dockerfile above, that is
rails server -b 0.0.0.0, meaninig that exec
rails server -b 0.0.0.0 will be executed within the entrypoint. The question here is: why do we need the entrypoint then? And the answer is because the container might have a pidfile that could make the cmd fail. That’s why
rm -f /app/tmp/pids/server.pid needs to exist in the entrypoint.
Are we ready to build our image and start runnning our app? No, we’re not. We still haven’t defined what’s our database, where it is, and we haven’t set up our
config/database.yml file. In our case, we’ll use a postgresql database. The reason why we didn’t do that yet is because we don’t want the database to be part of our app setup. If we do so, for example by installing it in our image, then our data won’t be persistent across different executions (technically, you can do that by mounting volumes and such, but it’s against best practices). This is where
docker-compose takes place.
version: "3" services: db: image: postgres volumes: - ./db/postgresql-data:/var/lib/postgresql/data web: build: . depends_on: - db ports: - 3000:3000 volumes: - .:/app
Step 2: docker-compose
As mentioned before, we don’t want our database to run separtedly from our codebase. In a typical Rails app, we’d have this
docker-compose.yml file in root directory of our Rails app:
Again, if you’re familiar with
docker-compose, it’s very easy to understand. We defined 2 services, the app itself (
web) and the postgresql database (
db). The key here is that we’re mounting 2 volumes:
. (our code!) will be mounted in
/app in our container and
./db/postgresql-data will be mounted in our db service in
/var/lib/posgresql/data. This is important as we want our db to be persisted across executions. In other words, we don’t want to run
rails db:create db:migrate every time we start our containers. Also, be aware that you don’t want
./db/postgresql-data to be versioned, so make sure you ignore it.
The next step is setting up
config/database.yml. We’ll do it this way:
default: &default adapter: postgresql pool: 15 timeout: 5000 username: postgres host: db development: <<: *default database: app_development test: &test <<: *default database: app_test production: <<: *default database: app_production
Note that this was redacted to keep it short. The important bits here are the
username, set to
postgresql and the
host, set to
db. Nothing here is azarous:
postgres is the username the postgres image defined for the username, and
db is the name of the service defined in our
docker-compose.yml file. If you want to find out more about the postgres image, you can do that by clicking here.
Are we ready to build our image and start running our app? Technically, yes, but let’s do one more thing.
Step 3: .dockerignore
You might guess what a
.dockerignore file is. If you thought it was a file that ignores the files and directories listed there when copying from the current context to the image, then you guessed correctly. We don’t want certain files to be copied onto our image to keep it small. In our typical Rails app, we’d have the following
.git db/postgresql-data log
Are we ready to build our image and start running our app? Yes! Now we are.
Step 4: building and running
Now that everything’s in place we can build our image and run our containers!
You can build your images and start the app by running:
$ docker-compose up --build
or the same command without
--build, as it would do it by default if the images doesn’t exist.
By the time it finishes, you should be up and running, and able to see your app at http://localhost:3000.
Join us in the next episode!