This is just a quick overview of using Docker and Docker Compose to spin up a solid development environment.
- 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
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
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 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.
ENVsets environment variables (key=value) which we can use in later instructions or the container itself
RUNallows us to run any command. Here we are using Alpine Linux’s dependency managment tool
yumon 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
appdirectory that will hold our rails project
WORKDIRsets up the working directory for any instructions that follow
COPYcopy our Gemfiles from our host’s current directory to the working directory of our container
- Then we install
bundlerand 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:
EXPOSEinforms 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
CMDentry 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 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.
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
The important difference here is:
-v $(pwd):/app - which tells docker to mount the current working directory to a folder on the container at
Now if we run the same
ls app/controllers command we will see our generated controller
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
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
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
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 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
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
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?
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 container
docker-compose run db psql -h db -U postgres- connect psql to our running database
docker-compose run web bin/rails console- open a rails console (works for any rails command)
Feel free to Tweet any errors to me @ashconnor