With the official release of Docker for Mac, it’s possible to completely containerize your development environment. When implemented in a Rails app, anyone with Docker for Mac can simply pull your repository and start developing with a single command.
Docker for Rails Development
You’ll want the following components:
- A Ruby container with bundler that will hold your app’s code and run the Rails server.
- A database container that runs whatever database you are using (we’ll use Postgres).
- A volume that contains all of the installed gems. You can re-use this volume across multiple apps so that you don’t need to re-install shared gems over and over. The volume will also persist gem installs even when you destroy the container.
The gems
Volume
Docker now has built-in support for volumes. At Metova, we use a volume named “gems”:
docker volume create --name gems
You can think of docker volumes like hard drives that are mountable inside any container at run-time; for example, if I ran:
docker run -v gems:/gems -ti ubuntu /bin/bash
I would be interactively running bash inside of an Ubuntu 14.04 container. When I cd into /gems, the contents of my “gems” docker volume will be there.
The Dockerfile
You’ll need a Dockerfile in the root of your app. I recommend using the official Ruby container as the base for your Rails containers. Using Ubuntu and then installing Ruby using unofficial sources or rbenv is a mess that is usually more trouble than it’s worth.
DockerfileFROM ruby:2.4.0 # replace with whatever Ruby version you are running # Most of these deps are for running JS tests. You can add whatever extra deps your project has (ffmpeg, imagemagick, etc)RUN apt-get updateRUN apt-get install -y build-essential nodejs qt5-default libqt5webkit5-dev xvfb postgresql postgresql-contrib # Point Bundler at /gems. This will cause Bundler to re-use gems that have already been installed on the gems volumeENV BUNDLE_PATH /gemsENV BUNDLE_HOME /gems # Increase how many threads Bundler uses when installing. Optional!ENV BUNDLE_JOBS 4 # How many times Bundler will retry a gem download. Optional!ENV BUNDLE_RETRY 3 # Where Rubygems will look for gems, similar to BUNDLE_ equivalents.ENV GEM_HOME /gemsENV GEM_PATH /gems # You'll need something here. For development, you don't need anything super secret.ENV SECRET_KEY_BASE development123 # Add /gems/bin to the path so any installed gem binaries are runnable from bash.ENV PATH /gems/bin:$PATH RUN gem install bundler # Allow SSH keys to be mounted (optional, but nice if you use SSH authentication for git)VOLUME /root/.ssh # Setup the directory where we will mount the codebase from the hostVOLUME /appWORKDIR /app CMD /bin/bash
This Dockerfile is a good base for developing Rails applications.
Docker Compose
Docker Compose is a tool that lets you define multiple containers and volumes in a simple YAML configuration file. It will also let you persist the command flags so that you don’t need to type it out or write scripts. For this example we’ll only hook up the database, but you can add other dependencies similarly depending on your app’s needs. Here is a docker-compose.yml that you can place in your app’s root directory, right next to the Dockerfile:
docker-compose.ymlversion: '2'services: dev: build: . environment: DB_HOST: db # The "db" hostname will point at the container named "db" automatically (defined below) # The following can be changed to whatever you want DB_NAME: my-app DB_USERNAME: dbuser DB_PASSWORD: password123 ports: # This is so you can go to localhost:3000 like you're used to. Otherwise, a port would be randomly mapped. - "3000:3000" volumes: # This will mount your "gems" volume (defined below as your external gem volumes) onto /gems - gems:/gems # This will mount ., your app code, into the /app directory within the container - .:/app # Mount your SSH keys into the container to allow git push (optional) - $HOME/.ssh:/root/.ssh db: image: postgres environment: # These variables are special to the official PG image, see: https://hub.docker.com/_/postgres/ POSTGRES_DB: my-app # match DB_NAME from above POSTGRES_USER: dbuser # match DB_USERNAME from above POSTGRES_PASS: password123 # match DB_PASSWORD from above volumes: # Same idea as gems above, this will prevent you from losing DB data when re-creating the container - db:/var/lib/postgresqlvolumes: db: {} gems: # Flagging this as "external" means it will use the volume named "gems" on the host instead of making a new volume specific to this docker-compose file external: true
In config/database.yml, you’ll want to configure your database to use the environment variables defined in the docker-compose file (this is good practice anyway, even without Docker!):
config/database.ymldevelopment: adapter: postgres host: <%= ENV['DB_HOST'] %> username: <%= ENV['DB_USERNAME'] %> password: <%= ENV['DB_PASSWORD'] %> database: <%= ENV['DB_NAME'] %>
Caveats
There are two caveats to developing Rails with Docker and Docker Compose.
1. Docker Compose does not map ports (defined in the ports: block) when using docker-compose run. To map them, you must run the command with the –service-ports flag. To avoid typing this everytime, I add this simple script in bin/docker:
bin/dockerdocker-compose run --service-ports dev
2. When running the Rails server, you need to bind it to 0.0.0.0 instead of the default localhost. I make another script in bin/server:
bin/serverbin/rails s -b 0.0.0.0
Remember to chmod +x these files!
End Result
Assuming Docker for Mac is installed, anyone can pull your repository and run:
bin/docker
This will pull all of the images and build the app. It could take some time on the first run if they need to download the Ruby container you are using as the base image. After it’s booted up, they will be at a bash prompt with everything they need. From there, they can run:
bundlebin/rake db:migratebin/server
The app will now be running on localhost:3000. Code changes will be applied without restart just as in normal development.