
Deploy Rails applications using Docker
- Transfer
Introduction
This post is about how I deployed the Ruby On Rails application to the DigitalOcean server so that it works in a separate Docker container. For simplicity, I’m going to explain in great detail the process of deploying a Rails application inside a Docker container.
In this post:
- How I installed Docker on the server
- Dockerfile for my Rails application
- Build with gems from Gemfile
- Build with compiled assets
- Running an application in Docker
- Docker environment variables for database.yml
Let's start by installing on the server.
Install Docker on the server
First, I downloaded the new Ubuntu 14.04 on DigitalOcean and installed Docker:
workstation $ ssh root@178.62.232.206
server $ apt-get install docker.io
server $ docker -v
Docker version 1.0.1, build 990021a
Dockerfile and nginx.conf
Now we need to assemble the Docker image from the Rails application. It so happened that Jeroen (Jeroen van Baarsen, approx. Transl.) Wrote about this last week: How I put together a Docker image for a Rails application . I will use his post as a basis for further steps.
I will collect the image on the same server on which I want to subsequently host the application itself. I decided to do this because I want the application not to be in the public domain, so a public Docker repository is a bad option for this. I could set up a private repository for myself, but then I would have to support it, which I do not want to do at the moment. In this post, I discuss the easiest way to use Docker to host an application.
I added the following Dockerfile and nginx.conf configuration files to my intercity-website project:
Dockerfile
FROM phusion/passenger-ruby21
MAINTAINER Firmhouse "hello@firmhouse.com"
ENV HOME /root
ENV RAILS_ENV production
CMD ["/sbin/my_init"]
RUN rm -f /etc/service/nginx/down
RUN rm /etc/nginx/sites-enabled/default
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf
ADD . /home/app/intercity_website
WORKDIR /home/app/intercity_website
RUN chown -R app:app /home/app/intercity_website
RUN sudo -u app bundle install --deployment
RUN sudo -u app RAILS_ENV=production rake assets:precompile
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
As you can see, Dockerfile uses the base phusion / passenger-ruby21 image. It adds the Nginx config, application code, runs bundler to install gems, and precompiles assets.
nginx.conf
# This is the server block that serves our application.
server {
server_name intercityup.com;
root /home/app/intercity_website/public;
passenger_enabled on;
passenger_user app;
passenger_ruby /usr/bin/ruby2.1;
}
# This is the server block that redirects www to non-www.
server {
server_name www.intercityup.com;
return 301 $scheme://intercityup.com$request_uri;
}
Building an image for an application container
I have added these files to my repository. Now I am going to upload it to the server and assemble the container:
my_workstation $ git archive -o app.tar.gz --prefix=app/ master
my_workstation $ scp app.tar.gz root@178.62.232.206:
my_workstation $ ssh root@ 178.62.232.206
server $ tar zxvf app.tar.gz
server $ docker build --tag="intercity-website" app/
This command prints a lot of results and does a lot of things. When I first started docker build, it took several minutes. This is because Docker must download the base phusion / passenger-ruby21 image. This is done only once. After loading the base image, the process will continue according to my Dockerfile.
Now the docker images command shows my image:
server $ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
intercity-website latest 629f05f42915 3 minutes ago 1.011 GB
First time container launch
It is time to launch the application.
server $ docker run --rm -p 80:80 intercity-website
This command starts the container, displays some data, and finally displays the following line:
[ 2014-09-23 11:23:11.9005 113/7fb22942b780 agents/Watchdog/Main.cpp:728 ]: All Phusion Passenger agents started!
Now let's see if the application works correctly. Run the query using curl:
server $ curl -H "Host: intercityup.com" http://localhost/
We`re sorry, but something went wrong (500)
...
Oops, something went wrong. Judging by the log inside the container (for access to which I used docker-bash from Phusion), I forgot to create a database. So now I'm going to install MySQL on the server.
Database installation
I will use the standard MySQL server available in Ubuntu 14.04:
server $ apt-get install mysql-server
After installation, and setting the administrator password, I can create a database for the application:
server $ mysql -u root -p
mysql> create database intercity_website_production;
Query OK, 1 row affected (0.00 sec)
mysql> grant all on intercity_website_production.* to 'intercity' identified by 'rwztBtRW6cFx9C';
Query OK, 0 rows affected (0.00 sec)
After that, I changed /etc/mysql/my.cnf and also bind-address from 127.0.0.1 to my external IP address, 178.62.232.206. That way, Rails in my container can now use MySQL. In /etc/mysql/my.cnf, I replaced the line with bind-address with the following:
bind-address = 178.62.232.206
And restarted MySQL:
server $ /etc/init.d/mysql restart
Using environment variables to configure the database
I am going to use environment variables so that my container can use them for authorization in MySQL. To do this, I need to do two things: 1) Prepare the database.yml file in the repository for the use of environment variables. and 2) configure Nginx to pass these variables to the passenger process.
Here is my new database.yml prepared for environment variables:
production:
adapter: mysql2
host: <%= ENV['APP_DB_HOST'] %>
port: <%= ENV['APP_DB_PORT'] || "3306" %>
database: <%= ENV['APP_DB_DATABASE'] %>
username: <%= ENV['APP_DB_USERNAME'] %>
password: <%= ENV['APP_DB_PASSWORD'] %>
For these environment variables to work for my Rails application, I need to configure Nginx. This is because Nginx resets all environment variables except those that you define.
I added the rails-env.conf file to the Rails application:
env APP_DB_HOST;
env APP_DB_PORT;
env APP_DB_DATABASE;
env APP_DB_USERNAME;
env APP_DB_PASSWORD;
And also fixed the Dockerfile to add the rails_env file when building the container:
FROM phusion/passenger-ruby21
MAINTAINER Firmhouse "hello@firmhouse.com"
ENV HOME /root
ENV RAILS_ENV production
CMD ["/sbin/my_init"]
RUN rm -f /etc/service/nginx/down
RUN rm /etc/nginx/sites-enabled/default
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf
# Add the rails-env configuration file
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf
ADD . /home/app/intercity_website
WORKDIR /home/app/intercity_website
RUN chown -R app:app /home/app/intercity_website
RUN sudo -u app bundle install --deployment
RUN sudo -u app RAILS_ENV=production rake assets:precompile
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EXPOSE 80
Building an image with support for environment variables
I added a new Nginx config to the repository. Now I intend to rebuild the new version of the container:
workstation $ git archive -o app.tar.gz --prefix=app/ master
workstation $ scp app.tar.gz root@178.62.232.206:
workstation $ ssh root@178.62.232.206
server $ tar zxvf app.tar.gz
server $ docker build --tag="intercity-website" app/
Running rake with environment variables
After building the container, I can configure the database. In the following command, I use environment variables to pass database connection information to run rake db: setup. Notice that I added the -u app argument to the command. This argument is needed to make sure that rake db: setup is run as the app user inside the container.
server $ docker run --rm -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" -u app intercity-website rake db:setup
intercity_website_production already exists
-- create_table("invite_requests", {:force=>true})
-> 0.0438s
-- initialize_schema_migrations_table()
-> 0.1085s
Wow! It worked!
Running an application with environment variables
Now I can start the container with the same environment variables and try to access it from the browser to check if it works:
server $ docker run --rm -p 80:80 -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" intercity-website
When I open 178.62.232.206 , I see a Rails application that connects to the database, and also see the assets have been compiled and everything works. Victory!
Conclusion
This concludes the post where we:
- Installed Docker on the server
- Set up a dockerfile and build a container image
- Set up the database using environment variables
What's next?
I still have questions to answer. I and other developers at Intercity will write more about them. Here are some of the issues that need to be addressed:
- How to automate the deployment? Maybe use something like Capistrano?
- What do I need to get zero downtime? When I first stop and then start the container, to launch a new version of the application, the connections will be reset.
- Where to store environment variables for each of the applications that I am going to deploy to the server?
- How do I speed up container building? Do I need to run bundler and rake assets: precompile every time for every deployment?
I hope you enjoyed this post. I will be glad to advice and questions!
Thank you very much for your attention.