Rails on Docker: Getting Started with Docker and Ruby on Rails
Docker is a fantastic tool for isolating your app and its environment, and allows easy distribution and state-replication across multiple environments (dev, test, beta, prod, etc.). Using Docker can get rid of the "it works on my machine" problem, and help you to easily scale your app as it grows.
Docker is particularly great when your app has a lot of dependencies, or requires specific versions of libraries and tools to be configured.
In this tutorial, you'll learn how to take a basic Rails app and prepare it for use in a Docker container ("dockerise" it).
PreRequisites
For this tutorial, I'm using a simple Rails 5 application configured to use a PostgreSQL database. If you use a different database, you'll need to tweak a few of the files below.
You can use the following template to create a basic Rails application that is configured with a Dockerfile
and config/database.yml
as below:
$ rails new --database=postgresql --skip-bundle --template=https://gist.githubusercontent.com/cblunt/1d3b0c1829875e3889d50c27eb233ebe/raw/8fce95533db3a3be19cb1aa31054589d219433c2/rails-docker-pg-template.rb my-app
$ cd my-app
Database Configuration
We can make use of environment variables to configure the details for our app's database. You'll use this later so that your app's docker container can connect to a PostgreSQL container.
Edit your config/database.yml
configuration
(Note: You don't need to do this if you've used the application template above)
Update your app's config/database.yml
to use environment variables:
# config/database.yml
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: db
username: <%= ENV.fetch('POSTGRES_USER') %>
password: <%= ENV.fetch('POSTGRES_PASSWORD') %>
development:
<<: *default
database: my-app_development
test:
<<: *default
database: my-app_test
production:
<<: *default
database: my-app_production
Create a Dockerfile
With your app prepared, it's time to start using Docker. Let's start by creating a Dockerfile
. This is a plain text file that instructs Docker how to build an image for your application.
You use it to install dependencies, configure default environment variables, copy code into the container, and so on.
To keep your image size small, I prefer to use the alpine-linux Ruby base image. Alpine linux is a tiny linux distribution that's perfect for containers, and Docker provides a default ruby:alpine
base image that we can use.
Write Your Dockerfile
Let's start by creating a basic Dockerfile
for your rails app. In your app's folder, create the following Dockerfile
.
(Note: You don't need to do this if you've used the application template above)
What if I don't use PostgreSQL?
If you're using a different database server (e.g. MySQL), you'll need to tweak the Dockerfile to install the appropriate packages.
You can search for the correct package(s) using the following Docker command:
$ docker run --rm -it ruby:2.5-alpine /bin/sh -c 'apk update && apk search mariadb | sort'
...
mariadb-client-10.2.15-r0
mariadb-common-10.2.15-r0
mariadb-connector-c-3.0.4-r1
mariadb-connector-c-dev-3.0.4-r1
mariadb-dev-10.2.15-r0
...
mysql-10.2.15-r0
mysql-bench-10.2.15-r0
mysql-client-10.2.15-r0
...
With your Dockerfile
written, you can now instruct Docker to build an image for your app:
Build an image for your app
$ docker build . -t my-app
Once the image is built, we're ready to get started! You can spin up a new container based on your app's image using the following command:
$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app my-app
We're passing a few arguments to the Docker run command:
-it
is actually two arguments that allow you to interact with your container via the shell (e.g. to issue Ctrl+C commands).--env
allows you to pass environment variables into the container. Here, you're using it to set the database connection values.--rm
will instruct Docker to remove the container once it finishes (i.e. when you hit Ctrl+C).--publish
forwards port 3000 from the container to port 3000 on your host. This allows you to access the container as if it was running on your host (i.e.http://localhost:3000
).- Finally
--volume
instructs Docker to "mount" the current folder (from your host machine) into the container. This means as you edit code on your workstation, it will be available to the container. Without this, you'd need to recreate the container with every code change you make.
Once it's up and running, you can access your container by opening localhost:3000 in your browser.
Run a Database Container
Unfortunately, although the container runs successfully, if you try to access the app in your browser, it will crash with a database error.
could not translate host name "db" to address: Name does not resolve
At the moment, there isn't a PostgreSQL server available for the app too connect. We'll fix that now by spinning up a separate Docker container in which PostgreSQL will run:
ProTip Remember that in Docker, a container should be designed to do one thing (and one thing only).
In our case, we'll use two containers: one for our app, and one for our database (PostgreSQL).
Start a new container running PostgreSQL:
Hit Ctrl+C to stop (and remove) your running app container, then spin up a new container for PostgreSQL:
$ docker run -d -it --env POSTGRES_PASSWORD=superSecret123 --env DB_NAME=my-app_development --name mydbcontainer postgres:9.6
The -d
flag will detach the container from our terminal, allowing it to run in the background. We also give the container a name (mydbcontainer
) which we'll use below.
Using Single–Task Containers
Docker containers are disposable, and their single-purpose nature means that once they have "finished", they are stopped (and, optionally, removed).
This makes them perfect for running one-off tasks, such as rails commands (e.g. bin/rails db:setup
).
We'll do that now to setup your app's database on mydbcontainer
:
Run the rails db:migrate
task using a container:
Use the following command to spin up a copy of your app's container, run the bin/rails db:setup
task, and then shut down.
Note that you'll need to configure environment variables for the database connection (these are injected into the config/database.yml
file you edited earlier).
You'll also use the --link
option which allows the container to connect to PostgreSQL container that is running (mydbcontainer
), using the hostname db
:
$ docker run --rm --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --link mydbcontainer:db --volume ${PWD}:/app my-app bin/rails db:create db:migrate
The --rm
flag will remove (delete) the container once it has finished running.
Once that command has run, your app's database will be setup on the mydbcontainer
container. Finally, we can run our app!
Run your app!
Let's spin up a new container using our app's image. Notice that there are a couple of additional arguments when running the command:
$ docker run --rm -it --env RAILS_ENV=development --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=superSecret123 --publish 3000:3000 --volume ${PWD}:/app --link mydbcontainer:db my-app
=> Puma starting in single mode...
=> * Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
=> * Min threads: 5, max threads: 5
=> * Environment: development
=> * Listening on tcp://0.0.0.0:3000
=> Use Ctrl-C to stop
Open your browser to localhost:3000, and you should see your app running entirely on Docker!
Next Steps
Docker is a great tool for developing your applications. As time goes on, you can begin to move all components of your app (database, redis, sidekiq workers, cron, etc.) to Docker.
The next step is to use Docker Compose to declare all your containers, and how they should work together.
👋 Thanks for reading - I hope you enjoyed this post. If you find it helpful and want to support further writing and tutorials like this one, please consider supporting my work with a coffee!
Support ☕️