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
- Build simple Rails app
- Install Nginx
- Install Docker
- Dockerize Rails app and Nginx – separate containers
- Dockerize Rails app and Nginx – single container
- Dockerize Rails app into container and run Nginx on host
- 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:
1 |
$ rails new sample_rails_docker_app -O |
Next we add something simple: single controller with one action to display simple message, like “Hello World”.
1 |
$ rails g controller hello_world |
Open app/controllers/hello_world_controller.rb and add hello action:
1 2 3 4 5 6 7 8 9 |
class HelloWorldController < ApplicationController def hello render json: { message: "Hello world!" }.to_json end end |
The hello action reponds with simple “Hello world!” message in JSON format. Next, edit config/routes.rb:
1 2 3 4 5 6 7 |
Rails.application.routes.draw do get 'hello_world/hello' root 'hello_world#hello' end |
Now you can test if everything works fine:
1 |
$ rails s |
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):
1 |
rails s -b xxx.xxx.xx.xx -p 3000 |
Open in web browser your app (e.g. http://localhost:3000 or http://<IP of machine>:3000). You should see this:
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:
1 2 |
$ sudo apt-get update $ sudo apt-get install nginx |
Check the nginx status:
1 |
$ systemctl status nginx |
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:
1 2 3 4 5 6 |
$ sudo apt-get update $ sudo apt-get install apt-transport-https ca-certificates $ sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D $ echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee /etc/apt/sources.list.d/docker.list $ sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual $ sudo apt-get install docker-engine |
More details about Docker installation you can find here.
To start docker deamon:
1 |
$ sudo service docker start |
To verify your docker deamon, use:
1 |
$ docker info |
Additional setup:
a) Create docker group and assign your system user. It allows to run Docker commands without sudo.
1 2 |
$ sudo groupadd docker $ sudo usermod -aG docker $USER |
Log out ad log in back to your shell.
b) Start docker deamon on boot:
1 |
$ sudo systemctl enable docker |
Docker compose
Install Docker Compose:
1 2 3 |
$ curl -L "https://github.com/docker/compose/releases/download/1.8.1/docker-compose-$(uname -s)-$(uname -m)" > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose $ docker-compose --version |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Base image: FROM ruby:2.3.1 # Install dependencies RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # Set an environment variable where the Rails app is installed to inside of Docker image: ENV RAILS_ROOT /var/www/sample_rails_docker_app RUN mkdir -p $RAILS_ROOT # Set working directory, where the commands will be ran: WORKDIR $RAILS_ROOT # Gems: COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN gem install bundler RUN bundle install COPY config/puma.rb config/puma.rb # Copy the main application. COPY . . EXPOSE 3000 # The default command that gets ran will be to start the Puma server. CMD bundle exec puma -C config/puma.rb |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# Base image: FROM nginx # Install dependencies RUN apt-get update -qq && apt-get -y install apache2-utils # establish where Nginx should look for files ENV RAILS_ROOT /var/www/sample_rails_docker_app # Set our working directory inside the image WORKDIR $RAILS_ROOT # create log directory RUN mkdir log # copy over static assets COPY public public/ # Copy Nginx config template COPY config/nginx.conf /tmp/docker_example.nginx # substitute variable references in the Nginx config template for real values from the environment # put the final config in its place RUN envsubst '$RAILS_ROOT' < /tmp/docker_example.nginx > /etc/nginx/conf.d/default.conf #RUN rm -rf /etc/nginx/sites-available/default #ADD config/nginx.conf /etc/nginx/sites-enabled/nginx.conf EXPOSE 80 # Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`) CMD [ "nginx", "-g", "daemon off;" ] |
Thirdly, we create docker-compose.yml, also in root directory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
version: '2' services: app: build: . command: bundle exec puma -C config/puma.rb volumes: - /var/www/sample_rails_docker_app expose: - "3000" web: build: context: . dockerfile: Dockerfile-nginx links: - app ports: - "80:80" |
Fourthly, we have to create configuration file for Nginx (in config folder): config/nginx.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
upstream puma_sample_rails_docker_app { 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 puma_sample_rails_docker_app; root /var/www/sample_rails_docker_app/public; try_files $uri/index.html $uri @puma_sample_rails_docker_app; location @puma_sample_rails_docker_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://puma_sample_rails_docker_app; # limit_req zone=one; access_log /var/www/sample_rails_docker_app/log/nginx.access.log; error_log /var/www/sample_rails_docker_app/log/nginx.error.log; } location ^~ /assets/ { gzip_static on; expires max; add_header Cache-Control public; } location = /50x.html { root html; } location = /404.html { root html; } location @503 { error_page 405 = /system/maintenance.html; if (-f $document_root/system/maintenance.html) { rewrite ^(.*)$ /system/maintenance.html break; } rewrite ^(.*)$ /503.html break; } if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){ return 405; } if (-f $document_root/system/maintenance.html) { return 503; } location ~ \.(php|html)$ { return 405; } } |
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:
1 2 3 |
.git .dockerignore Gemfile.lock |
BUILD AND RUN
Build (remember that you have to be in Rails project directory to run this command):
1 |
$ docker-compose build |
To verify that images were built:
1 |
$ docker images |
To run both containers via docker-compose:
1 |
$ docker-compose up |
Open it in web browser and check if you see the same message like in part 1:
In new shell window, run below command to see active containers:
1 |
$ docker ps |
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:
1 2 |
$ docker run -d --name app samplerailsdockerapp_app $ docker run -d -p 80:80 --name web --link app:app samplerailsdockerapp_web |
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:
1 |
$ docker stop 5086... |
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:
1 |
$ docker rm $(docker ps -a -q) |
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-docker. Its 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Base image: FROM ruby:2.3.1 # Install dependencies RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # Set an environment variable where the Rails app is installed to inside of Docker image: ENV RAILS_ROOT /var/www/sample_rails_docker_app RUN mkdir -p $RAILS_ROOT # Set working directory, where the commands will be ran: WORKDIR $RAILS_ROOT # Gems: COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN gem install bundler RUN bundle install COPY config/puma.rb config/puma.rb # Copy the main application. COPY . . EXPOSE 3000 # The default command that gets ran will be to start the Puma server. CMD bundle exec puma -C config/puma.rb |
docker-compose.yml:
1 2 3 4 5 6 7 8 9 |
version: '2' services: app: build: . command: bundle exec puma -C config/puma.rb volumes: - /var/www/sample_rails_docker_app ports: - "3000:3000" |
/etc/nginx/sites-enabled/nginx.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
upstream puma_sample_rails_docker_app { server 127.0.0.1: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 puma_sample_rails_docker_app; root /var/www/sample_rails_docker_app/public; try_files $uri/index.html $uri @puma_sample_rails_docker_app; location @puma_sample_rails_docker_app { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://puma_sample_rails_docker_app; # limit_req zone=one; access_log /var/log/nginx.access.log; error_log /var/log/nginx.error.log; } location ^~ /assets/ { gzip_static on; expires max; add_header Cache-Control public; } location = /50x.html { root html; } location = /404.html { root html; } location @503 { error_page 405 = /system/maintenance.html; if (-f $document_root/system/maintenance.html) { rewrite ^(.*)$ /system/maintenance.html break; } rewrite ^(.*)$ /503.html break; } if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){ return 405; } if (-f $document_root/system/maintenance.html) { return 503; } location ~ \.(php|html)$ { return 405; } } |
Start (or restart) nginx service:
1 |
$ service nginx start |
BUILD AND RUN
1 2 |
$ docker-compose build $ docker run -d --name app -p 3000:3000 samplerailsdockerapp_app |
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:
1 |
$ docker ps -a |
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:
1 |
$ docker rm CONTAINER_ID |
If you want to remove all containers, you can use:
1 |
$ docker rm $(docker ps -a -q) |
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:
1 |
$ docker images |
3) Tag specific image:
1 |
$ docker tag <image_id> <docker_hub_account>/<image_name>:<version> |
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:
1 |
$ docker tag 1e92da8c12cc tomaszantas/samplerailsdockerapp_web:1.0.0 |
You can list again docker images to check if the image was tagged.
4) Login to Docker Hub:
1 |
$ docker login |
5) Push docker image to Docker Hub:
1 |
$ docker push tomaszantas/samplerailsdockerapp_web |
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:
1 2 3 4 5 |
# Delete all containers $ docker rm $(docker ps -a -q) # Delete all images $ docker rmi $(docker images -q) |
To pull and run image, use following command:
1 |
$ docker run tomaszantas/samplerailsdockerapp_web:1.0.0 |
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:
1 |
$ docker run -p 80:80 tomaszantas/samplerailsdockerapp_web:1.0.0 |
In previous command we used version 1.0.0 of our app, but if you want to use the latest, use following version:
1 |
$ docker run -p 80:80 tomaszantas/samplerailsdockerapp_web:latest |
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
The area of his interest includes Popular Science, Internet of Things, Wearables, AI and Virtual/Augmented Reality.
Latest posts by Tomasz Antas (see all)
- Ruby on Rails with local and cloud AWS DynamoDB - February 6, 2017
- Rails 5: API-only Application – Quick Start - February 4, 2017
- Rails 5 and Docker (Puma, Nginx) - November 12, 2016