Rails 5 and Docker (Puma, Nginx)

Post presents how to set up simple Ruby on Rails application, build/run Docker image and push/pull it to/from Docker Hub repository. You will find also details how to run Nginx and Rails application in 3 configurations: in separate containers, in single containers and Rails app in container with Nginx running on host.

 

Rails: 5.0.0
Ruby: 2.3.1
Nginx: 1.10.0
Ubuntu 16.04 Xenial
Docker: 1.12.3

 

Introduction

The goal of this article is to prepare simple Ruby on Rails application and build basic Docker images. In this example, we use Puma as app (Rails) server which will cooperate with Nginx as web server. There are 3 ways how we can configure Nginx and Rails app together. Both can be placed in single container, in separate containers or nginx runs on host and Rails app is placed inside of container.

To keep focus on main purpose, the sample Rails application is built without any database (without Active Record).

The sample application can be developed on local machine or any VPS, EC2 instance, etc.. When Docker image with sample application will be ready, we will run it on AWS, but you can use any other service provider, environment and machine.

Before we continue, be sure that you have installed Ruby and Rails on you machine already.

 

Plan

  1. Build simple Rails app
  2. Install Nginx
  3. Install Docker
  4. Dockerize Rails app and Nginx – separate containers
  5. Dockerize Rails app and Nginx – single container
  6. Dockerize Rails app into container and run Nginx on host
  7. Push, pull and run Docker image on another host

 

1. Build simple Rails app

We assume that Ruby and Rails are already installed on your machine. If not, please install it now – you can find the instructions here.

Build simple Ruby on Rails application without database / Active Record:

Next we add something simple: single controller with one action to display simple message, like “Hello World”.

Open app/controllers/hello_world_controller.rb and add hello action:

The hello action reponds with simple “Hello world!” message in JSON format. Next, edit config/routes.rb:

Now you can test if everything works fine:

If you run on VPS (external machine), maybe you will need to add to this command the IP of your machine (replace xxx.xxx.xx.xx with the IP of your machine):

Open in web browser your app (e.g. http://localhost:3000 or http://<IP of machine>:3000). You should see this:

przechwytywanie

 

2. Install Nginx

In next step, we will add Nginx. Nginx web server collects request from the Internet and pass it to the Rails app server (Puma). Install nginx:

Check the nginx status:

 

 

3. Install Docker

Docker eco system consists of few tools, like Docker Engine, Docker Compose, Docker Toolbox, Docker Machine  and few more. Sometimes people think: Docker Engine = Docker, but in fact Docker Engine is only basic tool from the set of Docker instruments. We will install 2 Docker tools: Docker Engine and Docker Compose. Docker Engine is being used to manage individual containers. Docker Compose complements Docker Engine and it is useful especially in managing multi-container applications.

Docker

Install Docker Engine:

More details about Docker installation you can find here.

To start docker deamon:

To verify your docker deamon, use:

 

Additional setup:

a) Create docker group and assign your system user. It allows to run Docker commands without sudo.

Log out ad log in back to your shell.

b) Start docker deamon on boot:

 

Docker compose

Install Docker Compose:

 

 

 

4. Dockerize Rails app and Nginx – separate containers

In this configuration, Nginx and Rails are running inside different containers. Nginx will be available from Internet and will pass request to linked Rails app container. We will use following aliases:

  • web – for Nginx container,
  • app – for Rails app container.

We need to create:

  • 2 Dockerfiles – one for Rails app and one for Nginx
  • docker-compose.yml – it allows us to manage both services in one file
  • nginx.conf – nginx configuration file
  • .dockerignore (optionally) – it allows us to exclude some files, like .gitignore

 

CONFIGURATION

Firstly, we create Dockerfile file in main / root directory of Rails project:

Most of the lines from Dockerfile is probably quite clear. I’ll would like to add some explanation to FROM command. It points the base image from which our image is built. More images you can find here and base images for ruby you can find here.

Secondly, let’s create similar file for nginx: Dockerfile-nginx, also in project’s root directory:

Thirdly, we create docker-compose.yml, also in root directory:

Fourthly, we have to create configuration file for Nginx (in config folder): config/nginx.conf:

We can add optionally .dockerignore file, which is similar to .gitignore. We can set there a list of files and direcotries which should be excluded. For example, the content of file could be:

 

BUILD AND RUN

Build (remember that you have to be in Rails project directory to run this command):

To verify that images were built:

To run both containers via docker-compose:

Open it in web browser and check if you see the same message like in part 1:

przechwytywanie

In new shell window, run below command to see active containers:

You should see 2 containers on the list. Now, you can stop it by CTRL+C in shell window where you’ve run docker-compose up.

There is another method to run docker containers: docker run (…). In docker run we run each container separately. In docker-compose we have run all services defined in docker-compose.yml at the same time. But remember that there is also one more important issue: in docker-compose.yml we defined few things, which are not considered by plain docker (docker engine). So we have to remember about adding specific options to docker run command. For example, in docker-compose.yml file, under: services > web > links, we have placed information that web (nginx) service should be linked with app (rails) service, because we want to pass request from nginx to rails (from web container to app container).

Below is presented approach to run containers via docker engine:

You can again list running containers by “docker ps” command. To stop specific container, check its CONTAINER ID on the list from “docker ps” command and use it along with:

You can enter only 2-3 first characters of CONTAINER ID and (or) then use TAB to autocomplete the remaining part of ID.

To clear our machine and remove all containers:

 

You can find a source code on GitHub.

 

 

5. Dockerize Rails app and Nginx – single container

In this configuration, Rails app and Nginx should run together in a single container. Remember that Docker’s philosophy recommends to run one service per container, so we should rather run Nginx and Rails app in separate containers, like in (4). However, sometimes there may be some circumstances and running few services in a single container may be a good compromise in some cases.

In this case, I recommend to use baseimage-dockerIts approach is quite simple: It need one main process which will monitor other services. Other services we can define as subdirectories in /etc/services. Each requires run script inside own subdirectory.

I didn’t run into this case yet and I don’t have an example source code. However, I hope that information from this chapter gives you some complement to two other cases from chapters (4) and (6).

See also this Stackoverflow question.

 

 

6. Dockerize Rails app into container and run Nginx on host

In this configuration, Rails application will run inside of container, but nginx will be working directly on host machine – not in any container. In this scenario, nginx running on host will be pass request to 127.0.0.1:3000. On the other side, container with Rails app opens its port to 127.0.0.1:3000.

Dockerfile:

docker-compose.yml:

/etc/nginx/sites-enabled/nginx.conf:

Start (or restart) nginx service:

 

BUILD AND RUN

If you get error: “The name xxx is already in use by container …”  you have to rename your container or remove old one with the same name. To remove specific old container, first list all existing containers:

And find CONTAINER ID of container to remove (NAMES column can be useful to find right container). Next, use this CONTAINER ID to remove container:

If you want to remove all containers, you can use:

 

You can find a source code on GitHub.

 

 

 

7. Push, pull and run Docker image on another machine

In this part, we will push our image(s) to the Docker Hub repository. Then we will pull it on another machine and run it there.

More detials on Docker.

 

DOCKER HUB, TAG & PUSH

1) You need to create account on Docker Hub.

2) You have to add tag to image existing on our local machine. Get a list of images and check IMAGE ID:

3) Tag specific image:

image_id is the ID of image which we get in previous step. docker_hub_account is a your docker hub account. image_name is the name of the image and version is a version (like: 1.0.2 or latest). For example:

You can list again docker images to check if the image was tagged.

4) Login to Docker Hub:

5) Push docker image to Docker Hub:

6) Check in web browser your Docker Hub. New repository should be added.

 

PULL AND RUN IMAGE ON THE ANOTHER MACHINE

Remember to install Docker on the 2nd machine! … or instead using another machine, you can clear your local machine by removing all containers and images:

To pull and run image, use following command:

It first checks if image exists locally. If not it will download it from Docker Hub. Don’t forget to use also flags! Because I’ve tried to run our web sample app (this one with nginx), I have to add also a port flag:

In previous command we used version 1.0.0 of our app, but if you want to use the latest, use following version:

If you use our sample application and you will open it in web browser, you should see error: 502 Bad Gateway, and it is OK, because we run only web (nginx) service and we don’t run app (Rails) service. So, we now that nginx works itself, but it gives Bad Gateway, because cannot connect to Rails app.

 

 

 

Tomasz Antas

Ruby on Rails developer and Web designer.
The area of his interest includes Popular Science, Internet of Things, Wearables, AI and Virtual/Augmented Reality.

Latest posts by Tomasz Antas (see all)

Thanks for reading!

  • Jonathan Greenberg

    I notice that you are running your rails app with the ‘run’ command instead of ‘up’. I have a multi-service rails 5.0 application that I am running with docker-compose in development. I discovered that when starting the stack with ‘up’ changing code did not trigger a reload in any of the rails apps however if I run the front-end with the ‘run’ command (which also automatically starts the dependent rails api servers in the background) then code changes on any of the services do get reloaded to the servers. I am curious why there is this difference and if you found the same issue.

  • Jaye Freymann

    Great post, silly question – what’s the contents of your Puma.rb file?

    • Tomasz Antas

      Thanks! In puma.rb is just one important thing and depends on nginx.conf: how nginx (web server) and puma (app server) are connected together. There are two options:

      a) via port (like in this example)
      b) via socket

      In case of port, nginx.conf should have:

      upstream puma_sample_rails_docker_app {
      server 127.0.0.1:3000;
      }

      In docker-compose:

      expose:
      – “3000”

      In Dockerfile:

      EXPOSE 3000

      puma.rb should contain:

      port ENV.fetch(“PORT”) { 3000 }

      In case of socket:

      nginx.conf:

      upstream appserver {
      server unix:///var/run/puma.sock;
      }

      puma.rb (the socket address):

      bind ‘unix:///var/run/puma.sock?umask=0000’

      And you should start the Rails app in Dockerfile with following command:

      $ CMD bundle exec puma -C /app/config/puma.rb

      • Jaye Freymann

        Ahh, makes sense. Thanks for the info!