Welcome to the third post in the Ruby on Rails Developer Series. In this part, our goal is to add a Docker container to our application so we can deploy our packaged application on a server. This will enable us to create a continuous integration process while building our environment in a matter of seconds. We'll cycle through learning about the benefits of Docker and how to build a basic Docker image. The series theme is to make you feel confident as an engineer in building a structured project with Ruby on Rails.
You can do this!
What is Docker?
Docker allows you to package up an application or service with all of its dependencies in an image. The Docker image contains code, system libraries and whatever else you may need to install on a server.
What makes Docker different from a virtual machine?
Regarding a virtual machine (VM), you tend to have an entire guest operating system for each application you want to isolate. The time to reboot a server is also a longer process than being able to spin up a server in a few seconds. I think that says everything - when you can test your application in minutes vs hours.
What are the benefits?
Encapsulation of your Application
Expand Development Teams
Scalability
Bottom Line
Well, it is safe to assume that we can spin-up another server in seconds .. if it was Pokemon Go .. we wouldn't have to waste time dealing with non-trivial developer workarounds.
Let's begin!
PreRequisites
- Install Docker - Build our Dockerfile - Map out our application for our Docker Compose file - Spinning up our application!
Docker Compose relies on Docker Engine and we will need to set up our machine with docker to begin:
Install Docker below with your given OS:
This will install Docker and the tools including Docker Compose and other supporting apps.
Once installed, it's time to set up our Rails/PostgresSQL application in our Dockerfile.
Define the project
We will need to create a file named Dockerfile
:
I would reference this page for Linux apt-get command information. And for basic bash command understanding - I would recommend here
FROM ruby:2.5 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client RUN mkdir /myapp WORKDIR /myapp COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN bundle install COPY . /myapp # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"]
Entry point or entrypoint?
An entrypoint file
is a script based file ran when a container is started. We will use an entry point to fix a rails-specific issue that prevents the server from restarting when server.pid
pre-exists.
entrypoint.sh
#!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@"
What do these commands do?
set -e
- allows you to change the values of shell options and set the positional parameters, or to display the names and values of shell variables. And -e
is set to exit immediately if something returns a non-zero status.
rm -f
- allows you to remove a file path that is set. And -f
is to force the removal even if it has dependencies or will cause issues.
exec "$@"
- typically used to make the entry point a pass-through that then runs the Docker command. It will replace the currently running shell with the command that $@
is pointed at ($@)
.
How does the magic happen for development locally?
docker-compose.yml
is where we describe the services the application contains. One service we will include is the postgres database
and rails application
, redis
, possibly sidekiq
. Although, this file will configure and bind the services together to properly execute expose the correct ports, etc.
version: '3' services: db: image: postgres volumes: - ./tmp/db:/var/lib/postgresql/data web: build: . command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" volumes: - .:/myapp ports: - "3000:3000" depends_on: - db
Build the project
docker-compose run web rails new . --force --no-deps --database=postgresql
Context: We're building the image for the web service which utilizes the Dockerfile. Then it runs rails new inside the container and we generate a fresh app.
You should see a list of files like below:
If you add gems in your Gemfile, you will need to rebuild with the following command: docker-compose build
.
Modify Database Config
default: &default adapter: postgresql host: localhost port: 5432 encoding: utf8 pool: 5 database: rails-json-api username: development password: development development: &default adapter: postgresql host: localhost port: 5432 encoding: utf8 pool: 5 database: rails-json-api-dev username: development password: development
Create database and run application!
docker-compose run web rake db:create
Thats it!
You should be able to view http://localhost:3000
on the web browser to see the rails application boot up.
Overall, it really was that simple to dockerize our rails application! It could be a scary area, in my opinion, to enter for newer developers, although it really does help scale companies quickly.
In the last part of this series, we're going to explore how to secure your application with best practices and some cool gems that keep the community very strong!
Additional resources
Spin up a JSON API with Ruby on Rails
Read about the power of strong APIs
What's the difference between JSON and XML