Rails on Docker
Rails and Docker are important components in the development processes at Zegetech. Rails is our chosen platform for most of what we build, and docker provides pain-free environment management both for development and in production. We have previously covered these two technologies separately, and this post covers the sweet spot at their intersection. We will take you through the process of configuring a rails development environment on docker, and configure a postgresql database for it.
You might want to take a look at our previous blogs for some background information on what we will be setting up.
We want to build an app and have chosen Ruby on Rails as our framework. But because docker is awesome and a key component in developing and keeping things clean, we want to set up a development environment that allows us to work on a Rails application that resides in a docker container on our local machine. At the end of it, well have
- A rails app with a postgresql Database
- No local ruby, rails or postgresql dependancies on our machine. Everything will be dockerized.
- The command
docker -vshould work
- The command
docker-compose -vshould work
- The command
git --versionshould work
- Refer to the installation docs to upgrade if neccesary.
The Rails Docker image
In order to start developing, we need a docker container with the rails environment. We’ll build an image for that. Docker keeps track of your image as you make edits in the docker cache. In order to track, we need the main files that manage the rails application as well as the main docker files. First thing you need according to docker compose are the following files
Dockerfileusing whatever is the latest version of ruby on alpine. We use alpine because we want the smallest footprint possible for out app. If you change to another distro like ubuntu, then make sure you use the appropriate package manager e.g. apt-get instead of apk
Gemfilewith rails declaration
Gemfile.lockthat will be blank
docker-composeto make running docker commands easier
.dockerignorefile to exclude certain files from the build. This helps keep the image small.
These four files need to reside in the folder where our app will be. Details of what the commands do are included in the comments alongside the commands.
# Dockerfile FROM ruby:2.5.1-alpine LABEL maintainer="Kariuki Gathitu <email@example.com>" LABEL version="1.0" # Packages needed to get Rails running in Alpine. # DB_PACKAGES="sqlite-dev postgresql-dev mysql-dev" \ ENV BUILD_PACKAGES="curl-dev ruby-dev build-base bash" \ DEV_PACKAGES="zlib-dev libxml2-dev libxslt-dev tzdata yaml-dev" \ DB_PACKAGES="postgresql-dev postgresql-client" \ RUBY_PACKAGES="ruby-json yaml nodejs" # Update and install base packages RUN apk update && \ apk upgrade && \ apk add --update\ $BUILD_PACKAGES \ $DEV_PACKAGES \ $DB_PACKAGES \ $RUBY_PACKAGES && \ rm -rf /var/cache/apk/* && \ mkdir -p /usr/src/app # Create system user to run as non-root. RUN addgroup -S admin -g 1000 && adduser -S -g '' -u 1000 -G admin deploy # Set the Rails Environment Variables for production ENV RAILS_ROOT /home/deploy/app ENV RAILS_LOG_TO_STDOUT 1 ENV RAILS_ENV production # Set user as deploy from here on out USER deploy # Configure the main working directory. This is the base # directory used in any further RUN, COPY, and ENTRYPOINT # commands. RUN mkdir -p $RAILS_ROOT WORKDIR $RAILS_ROOT # Copy the Gemfile as well as the Gemfile.lock and install # the RubyGems. This is a separate step so the dependencies # will be cached unless changes to one of those two files # are made. COPY --chown=deploy:admin Gemfile Gemfile.lock ./ RUN gem install bundler RUN bundle install --jobs 20 --retry 5 # Copy the main application. COPY --chown=deploy:admin . ./ # Expose the applications port to the host machine EXPOSE 3000 # Command to run when the container is started. CMD ["puma", "-C", "config/puma.rb"]
We run the Dockerfile installation commands as a new
deploy user, which is the recommended practice. And when we copy files across, in order to avoid permission issues, we make sure the copy user is the non-root user we created.
# Gemfile source 'https://rubygems.org' gem 'rails'
# docker-compose.yml version: '3' services: app: build: . command: 'puma -C config/puma.rb' stdin_open: true tty: true environment: - RAILS_ENV=development ports: - '80:3000' links: - postgres volumes: - .:/home/deploy/app postgres: image: postgres:9.6.2-alpine environment: - POSTGRES_PASSWORD=mysecretpassword ports: - '5432' volumes: - data:/var/lib/postgresql/data volumes: data:
Mapping volumes in the app service,
.:/home/deploy/app only happens when running with docker compose. This is so that the developer can have realtime interaction with the app in the container when developing. Otherwise, you’d need to restart the container everytime you made changes. This however does not extend to the initial files we had, mainly the
Gemfile. Any Change to the
Dockerfile will require a rebuild of the container. We have also used a different service for the database. Postgres will reside in its own container. We are also binding the container port
3000 to our host machines (your laptop) port
# .dockerignore **/.git* **/*.sqlite3 **/*.sqlite3-journal log/* tmp/* storage/* **/README.md secrets/* **/docker-compose.yml **/Dockerfile
1. Generate New Rails app
Now that we have our configuration done, all that is left is to generate the rails app using docker compose
docker-compose run --no-deps app rails new . --force --database=postgresql # For API only app docker-compose run --no-deps app rails new . --force --database=postgresql --api #then rebuild image due to new Gemfile docker-compose build
This generates a new rails boilerplate in the current directory. Because our volumes are mapped, anything happening in the container is reflected on the host machine, so our directory will now have a brand new rails application. The
--no-deps flag tells compose not to start dependent services, in this case the postgres service.
2. Connect the database
We have the postgres DB in its own container with the official image from docker hub. We need to point our app to this database instance.
# app/config/database.yml default: &default adapter: postgresql encoding: unicode pool: 5 # Database credentials host: postgres # name for db service in docker-compose.yml username: postgres # default user for postgresql docker image password: mysecretpassword # must match POSTGRES_PASSWORD in docker-compose.yml
Now start the app daemonized
docker-compose up -d
The app will start in development mode because the docker-compose.yml file overrides the env
RAILS_ENV. In another window, initialize the DB.
docker-compose exec app rails db:create # or if the container was not yet started docker-compose run app rails db:create
The database file are persisted in the
data: docker volume. Without it you would need to run
docker-compose run web rake db:create whenever restarting your app to recreate the database.
Your app should be available at localhost.
To stop the application run
If all went well, then we need to “save” our app in git. Stop the app and check it into git
docker-compose down git add . git commit -m 'Version 0.0.0 - App Initialized'
So now we have our app boilerplate ready with persistence on a separate postgresql database.
As beautiful as the rails welcome page is, it doesn’t tell us if our environment behaves as we need it to. Let’s do some quick scaffolding to test.
First bring the app back up with
docker-compose up, then:
docker-compose exec app bundle exec rails g scaffold user username first_name last_name phone:integer
Then run the migrations:
docker-compose exec app bundle exec rails db:migrate
And edit our routes to point to our list of users:
Rails.application.routes.draw do resources :users root "users#index" # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end
Reload the page and voila:
You can add a few users to populate the page.
Finally, lets shutdown our server and see if it comes back up with what we expect.
Ctrl-C to bring down the services and wait for them to exit. Restart with a
If everything went well you should be greeted with the list of users you created. And …, breathe!
Avoid If You Can
- Building a custom image - this is all well and good for the learning experience or when there isn’t a ready image on dockerhub. Otherwise, it’s a pain not worth having.
- Installing postgres in the rails container - two reasons why; using a separate database image means you can re-use it in a new app, and second, making postgres work in the app container is a major hustle. You’ll need to set up the postgres user, set up authentication and find a way to make sure the db service is started when you run your app.
- Bind mounts for 3rd-party application data - use docker managed volumes for data you don’t need to interact with directly. Docker sets up the volumes so that the container has the proper access rights, helping avoid a world of pain in managing file permissions.
And that’s that… finally. We have our environment set up and can finally bring that app to life! Whatever you choose to build, ganbatte.