How to Run Rails App With Postgres Puma and Nginx in Docker


Ruby on Rails Docker


Table of contents:

I would probably need to start this post with “why?” entry, however I leave it for other nice articles around this topic. I recently moved all my development from local environment (mac) to containers and have moved this web-site in production to containers (3 containers) with AmazonEC2 driver for docker-machine. This post contains boilerplates for Rails, PostgreSQL, nginx and some other configuration parts. I will be using Docker Compose.

Install Docker on mac #

I don’t think this needs to be explain since you can check this and it’s done in 2 minutes. If you do Docker for other operation system, check the same link, there are options for Windows and Linux environments. I would move to the next step once you have the below outputs as well (versions might be slightly different since they are rolling out updates frequently):

1docker -v
2Docker version 17.06.0-ce, build 02c1d87
3docker-machine -v
4docker-machine version 0.12.0, build 45c69ad
5docker-compose -v
6docker-compose version 1.14.0, build c7bdf9e

If you see all three outputs without any errors, feel free to join me in the next step.

Docker-Machine AWS #

While everybody can do local docker-machine or Virtualbox, I’d love to go ahead and setup my docker-machine in AWS. It is also super simple and it is explained here. You will need AWS IAM user with Programmatic access (for AWS API, CLI, SDK) with at least AmazonEC2FullAccess permissions. I have briefly talked about storages and IAM policies in my post here (it is actually review of recent webinar). If you are not familiar with it, I would suggest you to check fundamentals of AWS from CloudAcademy). If you don’t use elastic IP for your EC2 instance (your new docker-machine) keep this command with you every time when you launch it: docker-machine regenerate-certs your_machine_name since it needs to regenerate cert for new destination. This is the major reason you get: Unable to query docker version: Get https://XX.XX.XX.XX:2376/v1.15/version: x509: certificate is valid for YY.YY.YY.YY. Just regenerate your cert and you are good to go.

Setting up new Rails App and Dockerfile #

First let’s create our new application with Postgres for backend. Let’s use scaffolding and generate (g) our sample model Post with few arguments including avatar processed by this popular gem for images.

1rails new my-application --database=postgresql
2rails g scaffold posts
3gem install paperclip // this is if you have not installed this gem yet, otherwise, ignore.
4rails g paperclip posts avatar // since paperclip comes with generator we can use it. Use "rails g" to get list of available generators and their options.

Let’s change directory and jump into our new app folder (cd my-application) and create our Dockerfile and it should contain the following:

 1FROM ruby:2.3.1
 2MAINTAINER Your Name <your@email.here>
 3RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs imagemagick
 4ENV RAILS_ROOT /var/www/my-application
 5RUN mkdir -p $RAILS_ROOT
 6WORKDIR $RAILS_ROOT
 7COPY Gemfile Gemfile
 8COPY Gemfile.lock Gemfile.lock
 9RUN gem install bundler
10RUN bundle install
11COPY . .
12EXPOSE 3000
13CMD bundle exec puma -C config/puma.rb

Just to go through the configuration file quickly really:

Here is what yo need to drop in your config/puma.rb:

 1workers Integer(ENV['WEB_CONCURRENCY'] || 2)
 2threads_count = Integer(ENV['MAX_THREADS'] || 5)
 3threads threads_count, threads_count
 4
 5preload_app!
 6
 7rackup      DefaultRackup
 8port        ENV['PORT']     || 3000
 9environment ENV['RACK_ENV'] || 'production'
10
11on_worker_boot do
12  # Worker specific setup for Rails 4.1+
13  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
14  ActiveRecord::Base.establish_connection
15end

You can check your single image (Rails and Puma) by sending this to your bash console: docker build .. It should build the image with Rails (you’ll see apt-get install and bundler doing it’s tasks. This insures you’ve done things right and you don’t have any issues. Once image is ready, go ahead and check docker images. You’ll see your new image at the top of the list and it comes with no tags and repository. We don’t need repository at the moment, but let’s tag it by docker tag dc407865d8f7 your_tag_string (change random 12-symbol string to yours, it is IMAGE ID from the list command). It should give it your_tag_string and next time when you list images you can identify it. Easy, right?!

We are ready for the next step.

Docker nginx #

Now we need to setup our web-server to handle http requests. We can do it either by setting up nginx on the docker-machine host (in our case it is EC2). To do this we just need docker-machine ssh docker_machine_name and install nginx. Since AWS EC2 gives us Ubuntu, it is easy to follow the above guide. However I’d love to run nginx from docker hub image in one of my containers. Let’s create Dockerfile-nginx and drop the following code into it:

1FROM nginx
2MAINTAINER Your Name <your@email.here>
3RUN apt-get update -qq && apt-get -y install apache2-utils
4ENV RAILS_ROOT /var/www/my-application
5WORKDIR $RAILS_ROOT
6COPY config/nginx.conf /etc/nginx/conf.d/default.conf
7EXPOSE 80
8CMD [ "nginx", "-g", "daemon off;" ]

config/nginx.conf content is the following (pretty standard that we use for linking with puma).

 1upstream my-application {
 2  server app:3000;
 3}
 4server {
 5  listen 80;
 6  client_max_body_size 4G;
 7  keepalive_timeout 10;
 8  error_page 500 502 504 /500.html;
 9  error_page 503 @503;
10  server_name localhost my-application;
11  root /var/www/my-application/public;
12  try_files $uri/index.html $uri @my-application;
13  location @my-application {
14    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15    proxy_set_header Host $http_host;
16    proxy_redirect off;
17    proxy_pass http://my-application;
18    access_log /var/www/my-application/log/nginx.access.log;
19    error_log /var/www/my-application/log/nginx.error.log;
20  }
21  location ^~ /assets/ {
22    gzip_static on;
23    expires max;
24    add_header Cache-Control public;
25  }
26}

Of course there might be more things, but for the sake of simplicity we keep it as above. Feel free to customize it.

Docker-compose config file #

Almost done. Docker-compose allows us to define and run multi-container applications with Docker. Exactly what you were missing, right? Let’s see the syntax of this file, just touch docker-compose.yml file in your root folder of Rails App and drop the following content:

 1version: '3'
 2services:
 3  web:
 4    build:
 5      context: .
 6      dockerfile: Dockerfile-nginx
 7    links:
 8      - app
 9    ports:
10      - "80:80"
11  app:
12    build: .
13    volumes:
14      - /var/www/my-application
15    ports:
16      - "3000"
17    depends_on:
18      - postgres
19  postgres:
20    image: postgres:9.6
21    ports:
22      - "5432:5432"
23    volumes:
24      - ./postgres-data:/var/lib/postgresql/data

Here we have the following:

Another important point in our docker-compose file is depends_on attributes that makes sure we have postgres container is built first and exists.

Now we are ready for the first build using docker-compose. Let’s do this by typing in command line: docker-compose build. This should take some time (all three containers should be built). Next run them by docker-compose up -d. This should deliver containers to our docker-machine and launch them. Go ahead and check your AWS EC2 IP in your favorite web-browser (if you don’t remember it - check docker-machine ls). Make sure you have ACTIVE flagged by asterisk (*) otherwise it goes to local docker-machine (by the way to connect your shell to docker-machine: eval (docker-machine env docker01). Syntax may be vary, depends on your shell (I am using fish-shell). The above few commands with output for the validation purpose.

1docker-machine ls
2NAME       ACTIVE   DRIVER      STATE     URL                       SWARM   DOCKER        ERRORS
3docker01   *        amazonec2   Running   tcp://XX.XXX.XX.XX:2376           v17.05.0-ce
4docker ps
5CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
6adafcefde6fa        docker_web          "nginx -g 'daemon ..."   3 days ago          Up 2 minutes        0.0.0.0:80->80/tcp        docker_web_1
73175c611105c        docker_app          "bundle exec puma ..."   3 days ago          Up 2 minutes        0.0.0.0:32768->3000/tcp   docker_app_1
80e1f75fe9b61        postgres:9.6        "docker-entrypoint..."   3 days ago          Up 2 minutes        0.0.0.0:5432->5432/tcp    docker_postgres_1

Docker shut down #

To shut down all containers run by compose we do docker-compose down. To stop our docker-machine - docker-machine stop docker_machine_name. I keep it off since AWS charges on hour basis, however if it is new account for you in Amazon you might be getting free tier resources (since docker-machine AmazonEC2 adapter is launched as t2.micro and it is free tier for first 12 month). Learn Amazon Web Services free tier here.

Uncovered things (tips) #

If you are seeing pending Rails migrations (I have not setup entry points with bash scripts), but this is possible, instead, for rake tasks like migrations I do this:

1docker-compose RAILS_ENV=ENV run app rake db:migrate

If you need to access something in certain container (for example you want to see the logs output from your Rails App container) you can use exec prefix with docker followed by container ID and followed by shell’s command. For example if I want to tail my log file I do this:

1docker exec -i -t 3175c611105c cat /var/www/my-application/log/development.log

That’s it really.

comments powered by Disqus