Local Rails Development with Docker and Docker Compose
This is just a quick overview of using Docker and Docker Compose to spin up a solid development environment.
Assumptions
- You’re using OSX
- You have Docker for OSX installed
- You have ruby and the rails 5.0+ gem installed
That said, this should work on both Linux and Windows without any issues.
Create an empty rails application
First we need a rails application to test our Docker environment with, so run rails new myapp
to create a skeleton application and change into the new myapp
directory.
Creating a Dockerfile
Docker uses Dockerfiles as a blueprint to build images. This file will describe how we want the image to function both at build time and runtime.
To create a Dockerfile simply initialise an empty file with the name Dockerfile
.
Open this new file with your favourite text editor and add the following lines:
FROM
specifies which Base Image we want to build from. I’ve chosen the ruby
image version 2.3-alpine
. 2.3
is the ruby language version and alpine
refers to Alpine Linux which is a lean Linux distribution that helps keep our Docker images small.
A full choice of ruby versions and distro varients can be found on Dockerhub.
Next we need to install some dependecies that will help us install rails gems that require native extensions such as the postgresql gem.
ENV
sets environment variables (key=value) which we can use in later instructions or the container itselfRUN
allows us to run any command. Here we are using Alpine Linux’s dependency managment toolapk
(similar toapt
andyum
on other distros) to install our required packages
Next we create our working directories and copy in our Gemfile so we can install our project’s dependencies:
There are several instructions here so let’s break it down:
- First we are creating an
app
directory that will hold our rails project WORKDIR
sets up the working directory for any instructions that followCOPY
copy our Gemfiles from our host’s current directory to the working directory of our container- Then we install
bundler
and all our gems - Finally - we copy over our entire current directory and place the files in the Docker image’s work directory
We finish our Dockerfile adding these two lines:
EXPOSE
informs Docker that the container is listening for requests on the specified port, this port is not yet accessible by the host- There can only be one
CMD
entry per Dockerfile which is the default unless it is overridden
Building the Docker image
We can now build our image:
And test it by running the rails server command:
However if we attempt to connect to the container http://localhost:3000 it won’t work because we haven’t mapped the container’s port to a port on our host.
We can do this by adding the P
flag.
The P
flag binds the exposed ports on the container to random unpriviledged ports on the host. To get this random port we can run the docker ps
command.
Here we can see our myapp container port 3000 has mapped to our host port 32776. So if we visit http://localhost:32776 we can see the rails default home.
Sharing code between container and host
Let’s modify the default page with a “Hello World” to demonstrate how we can modify code on our host and have that run on our new container.
You may be tempted to run a rails g
command from your host, but we can run one off commands in another docker container like so:
After this command has finished, the container terminates.
But wait. The files we just generated aren’t available on our host filesystem.
If we ls
myapp’s app/controller
directory we would expect to find a file called /controllers/welcome_controller.rb
but it’s not there.
This is because we’ve yet to setup a shared filesystem between our host and container. So any modifications we make to the filesystem in our container are disgarded when the container terminates.
To fix this we can use a Docker concept called volumes
, which is a way to “mount” a host (or another container) directory to your container.
Let’s try this with our myapp
container:
The important difference here is: -v $(pwd):/app
- which tells docker to mount the current working directory to a folder on the container at /app
.
Now if we run the same ls app/controllers
command we will see our generated controller welcome_controller.rb
.
Let’s edit the application’s routes.rb
to use our new Welcome Controller.
Using Postgres as our database
Right now our application is using SQLite as the database which isn’t ideal as it probably differs to what we are using in production. It’d be great if we could run Postgres in another container and allow our rails application to use that.
To do this we need to make a network so our containers can communicate with each other:
Let’s run a Postgres container in another terminal and have it use our new network:
Docker will pull the image if you don’t have it already.
Notice we also gave the container a name db
. This makes it easier to connect the containers together.
To switch to Postgres we need to make several changes:
First we need to rebuild our dockerimage to include the Postgres development dependencies.
In our Dockerfile we need to replace sqlite-dev
with postgresql-dev
and rebuild our image using $ docker build --tag myapp .
.
We also need to update our Gemfile
to use the pg
gem. To do that replace gem 'sqlite3'
with gem 'pg'
.
Finally, let’s modify our application’s config/database.yml
file to use postgres:
Notice that the host
entry is populated with our container name db
.
Then run the Postgres container like so:
We can run the rails server on the same network:
If you get an error like docker: Error response from daemon: Conflict. The container name "/rails" is already in use.
simply remove the container using the name by running docker rm $containerId
- where $containerId is the ID output in the error.
Both containers are now running, but since we don’t have any database specific code in our application, let’s just create the empty databases in Postgres via rake to confirm that things are working.
It works!
docker-compose
It can be tedious to manually run multiple commands in different terminals in order to get containers to communicate together. Luckily there’s a better way. Enter docker-compose.
Docker compose allows us to create a single configuration file describing how we want our containers to be wired togther.
To do this, create a docker-compose.yml
file in the same directory as your Dockerfile.
Let’s look at what we have specified under the services
key.
First, we specify our container name db
and what image it should use. This is followed by a volumes array which contains only one volume. This maps Postgres’ data volume to the host directory data/postgres
.
We do this to prevent the Postgres container from losing all of its stored data when the container restarts.
Next is the web container, this is pretty much the same as it was before with the exception of a mapping of port 3000 on the host, to 3000 on the container. No more docker ps
to find out what port our app is running on. It will always be http://localhost:3000.
Finally, we add a dependency on db
which takes care of the connectivity between the two containers.
Now we can run our docker-compose file:
Docker compose has built our image and is now running that image along with a Postgres container, linking them both together. We can see the output streaming from containers in the console.
To stop all our containers we can run:
Adding Redis for ActionCable
Action Cable was shipped with Rails v5.0 allowing applications to take advantage of websockets.
When used in development Action Cable can use the sync
driver but when we move into production it’s recommended that we use Redis.
Personally, I think that development environments should be as close as possible to production. This can reduce those last minute environmental issues.
With that said, let’s expand our docker-compose file to make use of redis:
We need to make one change in our config/cable.yml
to connect to redis:
Now if we run $ docker-compose up
we will see redis also booting - along side our app and database.
That’s it! See how easy it is to add new services to our application?
Useful commands
We’ve seen how to run one-off tasks using docker. So here’s a few commands I’ve found useful:
docker-compose run redis redis-cli -h redis
- start a redis-cli and connect to our redis containerdocker-compose run db psql -h db -U postgres
- connect psql to our running databasedocker-compose run web bin/rails console
- open a rails console (works for any rails command)