Deploy multiple Rails apps with Passenger, Nginx, and Docker

Here’s the problem:

I’ve got a bunch of Rails apps, but only a handful of cloud servers. I need some of them to live on a single machine without them stepping all over each other.

Assumptions

This guide assumes the server is running Ubuntu 14.04 and has all the requisite software already installed (e.g.: Docker, Rails, etc.).

Enter Docker

Docker makes the following configuration easy to maintain:

[System Topology]

Docker is also nice because all the required containers come pre-packaged:

Nginx

First, get some SSL certificates

You’ll need one for each Rails app you wish to deploy. These can be self-signed or obtained from a Certificate Authority. To self-sign a certificate, execute the following:

1
2
3
4
5
6
mkdir certs
cd certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout sub.example.com.key -out sub.example.com.crt
cd ..
sudo chown -R root:root certs
sudo chmod -R 600 certs

Note the keyout and out options. The jwilder/nginx-proxy Docker image won’t pick up the certificates unless they are named in accordance with the production site’s URL and subdomain (if any). For example, if you have a certificate for example.com, the keyout and out options must be named example.com.key and example.com.crt respectively.

Obtain a certificate for each app you wish to deploy (or just get one for the purposes of this tutorial).

Then, run the Nginx docker image

Note the app username. Adjust as appropriate.

1
docker run --restart=always --name nginx-proxy -d -p 80:80 -p 443:443 -v /home/app/certs:/etc/nginx/certs -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy

PostgreSQL

1
docker run --restart=always --name postgres -e POSTGRES_PASSWORD=secretp@ssword -d postgres

Rails apps

Now for the tricky part…

This configuration is meant to make deployment easy. The easiest way I’ve discovered so far involves writing a Dockerfile for the Rails app and providing Nginx some configuration files.

Save this sample Dockerfile in your app’s root directory on the server (next to the Gemfile):

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
# Adapted from https://intercityup.com/blog/deploy-rails-app-including-database-configuration-env-vars-assets-using-docker.html
FROM phusion/passenger-ruby22:latest
MAINTAINER Some Groovy Cat "hepcat@example.com"
# Set correct environment variables.
ENV HOME /root
ENV RAILS_ENV production
# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]
# Start Nginx and Passenger
EXPOSE 80
RUN rm -f /etc/service/nginx/down
# Configure Nginx
RUN rm /etc/nginx/sites-enabled/default
ADD docker/my-app.conf /etc/nginx/sites-enabled/my-app.conf
ADD docker/postgres-env.conf /etc/nginx/main.d/postgres-env.conf
# Install the app
ADD . /home/app/my-app
WORKDIR /home/app/my-app
RUN chown -R app:app /home/app/my-app
RUN sudo -u app bundle install --deployment
# TODO: figure out how to install `node` modules without `sudo`
RUN sudo npm install
RUN sudo -u app RAILS_ENV=production rake assets:precompile
# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Note the ADD commands under the Configure Nginx header. These are copying configurations into the Docker image. Here I put them in the docker directory to keep them organized. From your app’s root directory:

1
mkdir docker

Now, save the following to docker/my-app.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name example.com;
root /home/app/my-app/public;
# Passenger
passenger_enabled on;
passenger_user app;
passenger_ruby /usr/bin/ruby2.2;
}

Of course, change the server name as appropriate. Also note the /home/app directory. app is the username set up by the phusion/passenger-ruby22 image.

Next, save the following to docker/postgres-env.conf

1
2
env POSTGRES_PORT_5432_TCP_ADDR;
env POSTGRES_PORT_5432_TCP_PORT;

This is some Docker magic that preserves these Postgres environment variables.

Now, build the app’s image from the project’s root directory:

1
docker build -t my-app-image .

This command reads the Dockerfile just created and executes the instructions contained therein.

Setup, migrate, and seed the database:

1
2
3
docker run --rm --link postgres:postgres my-app-image rake db:create
docker run --rm --link postgres:postgres my-app-image rake db:migrate
docker run --rm --link postgres:postgres my-app-image rake db:seed

Finally, execute the image:

1
docker run --restart=always --name my-app --expose 80 -e VIRTUAL_HOST=example.com --link postgres:postgres -d my-app-image

If everything goes well, you will be able to see your app at example.com (or wherever).

Next

Deploy a Rails app to Docker with Capistrano