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:
- 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 usepaperclip
for images processing and push them to AWS S3 - We also have set environment variable (
RAILS_ROOT
), created directory for our new application and copiedGemfile
. Then we usedRUN
to have bundler installed and install all gems according toGemfile
. COPY . .
ANDEXPOSE
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).
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:
- 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.
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.