How to run Rails app with Postgres, Puma and Nginx in docker

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):


docker -v
Docker version 17.06.0-ce, build 02c1d87
docker-machine -v
docker-machine version 0.12.0, build 45c69ad
docker-compose -v
docker-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.


rails new my-application --database=postgresql
rails g scaffold posts
gem install paperclip // this is if you have not installed this gem yet, otherwise, ignore.
rails 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:


FROM ruby:2.3.1
MAINTAINER Your Name 
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs imagemagick
ENV RAILS_ROOT /var/www/my-application
RUN mkdir -p $RAILS_ROOT
WORKDIR $RAILS_ROOT
COPY Gemfile Gemfile
RUN gem install bundler
RUN bundle install
COPY . .
EXPOSE 3000
CMD bundle exec puma -C config/puma.rb

Just to go through the configuration file quickly really:

  • FROM - means that we are going to use image with Ruby 2.3.1
  • RUN apt-get ... - this installs all required for Ruby / Rails framework dependencies. I also put imagemagick down the road since I want to use paperclip for images processing and push them to AWS S3
  • We also have set environment variable (RAILS_ROOT), created directory for our new application and copied Gemfile. Then we used RUN to have bundler installed and install all gems according to Gemfile.
  • COPY . . AND EXPOSE copies our application into our container and exposes port 3000
  • Finally we run puma with our configuration file from config/puma.rb (don't worry, you'll see it's content below).

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:


FROM nginx
MAINTAINER Your Name 
RUN apt-get update -qq && apt-get -y install apache2-utils
ENV RAILS_ROOT /var/www/my-application
WORKDIR $RAILS_ROOT
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]

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


upstream my-application {
  server app:3000;
}
server {
  listen 80;
  client_max_body_size 4G;
  keepalive_timeout 10;
  error_page 500 502 504 /500.html;
  error_page 503 @503;
  server_name localhost my-application;
  root /var/www/my-application/public;
  try_files $uri/index.html $uri @my-application;
  location @my-application {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://my-application;
    access_log /var/www/my-application/log/nginx.access.log;
    error_log /var/www/my-application/log/nginx.error.log;
  }
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }
}

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:


version: '3'
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile-nginx
    links:
      - app
    ports:
      - "80:80"
  app:
    build: .
    volumes:
      - /var/www/my-application
    ports:
      - "3000"
    depends_on:
      - postgres
  postgres:
    image: postgres:9.6
    ports:
      - "5432:5432"
    volumes:
      - ./postgres-data:/var/lib/postgresql/data


Here we have the following:

  • web - our nginx container configuration points to it's individual Dockerfile (we've made it earlier)
  • app - our Rails App container with scaffolded Model and paperclip generated avatar attribute
  • postgres - our DB for keeping records. We are going to keep it's data outside of the container, since docker containers are stateless and immutable. What means we need to backup postgres / keep our data somewhere else (database data, structure). In order to do this we use volumes. Since they are preferred mechanism for persisting data generated by and used by our containers.

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.


docker-machine ls
NAME       ACTIVE   DRIVER      STATE     URL                       SWARM   DOCKER        ERRORS
docker01   *        amazonec2   Running   tcp://XX.XXX.XX.XX:2376           v17.05.0-ce
docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
adafcefde6fa        docker_web          "nginx -g 'daemon ..."   3 days ago          Up 2 minutes        0.0.0.0:80->80/tcp        docker_web_1
3175c611105c        docker_app          "bundle exec puma ..."   3 days ago          Up 2 minutes        0.0.0.0:32768->3000/tcp   docker_app_1
0e1f75fe9b61        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:


docker-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:


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

That's it really.


In short, this is about:
#rails
#docker

Start discussion:
Related articles:
107 how to renew certbot let s encrypt for rails app with capistrano preview
Let's Encrypt is free SSL certificate. Running rails app it is not obvious how to renew such cert. Here is the user guide. ... more