Today we’re going to be improving deployment of the meal plan application by setting up a web server (Nginx) in front of our application server.
Goal for this Tutorial:
- Pre-compile assets as part of our image building process
- Add Nginx to our web application stack
- Handle Deploying New Application Changes
We current have our application running in the wild, but pointing a user at an IP address with a port number isn’t a great approach. It’s also not great that there is a really long first load time when the application has to compile the status assets (javascript, styles, etc). We can fix both of these issues by using a web server and pre-compiling our assets.
Before we get going too far though we’re going to improve our ability to run commands using our docker-compose.prod.yml
file. Let’s create script/prod
to make this a little easier to type:
script/prod
#!/bin/sh
docker-compose -f docker-compose.prod.yml "$@"
After you save the file don’t forget to make it executable:
$ chmod +x script/prod
Note: If you’re on windows you can created a console alias by following the instructions here
Pre-compiling Application Assets
We’re going to pre-compile assets as part of our Docker image build so that we can do it before we ever mess with our containers that are already running. This will make our application much faster on the first load and force our ruby process to do less once we have Nginx in place. We’ll start by creating a separate Dockerfile
for production.
$ cp Dockerfile Dockerfile.prod
We’re calling this Dockefile.prod
, but it would really work for any configuration environment that we want to have our assets pre-compiled into. Next, we’ll make a few changes to the file to get our assets to be pre-compiled:
Dockerfile.prod
FROM ruby:2.3.1
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
postgresql-client \
nodejs \
&& apt-get -q clean \
&& rm -rf /var/lib/apt/lists
# Pre-install gems with native extensions
RUN gem install nokogiri -v "1.6.8.1"
WORKDIR /usr/src/app
COPY Gemfile* ./
RUN bundle install
COPY . .
# Pre-compile assets
ENV RAILS_ENV production
RUN rails assets:precompile
CMD script/start
Next, we will need to set up our docker-compose.prod.yml
file to use the proper file to build our image.
docker-compose.prod.yml
version: "2"
volumes:
db-data:
external: false
services:
prod_db:
image: postgres
env_file: .env.prod
volumes:
- db-data:/var/lib/postgresql/db-data
prod_app:
build:
context: .
dockerfile: Dockerfile.prod
env_file: .env.prod
ports:
- "3000:3000"
depends_on:
- prod_db
Let’s build it and watch the assets be precompiled using our new script/prod
$ script/prod build prod_app
You can see that writes the values out to /usr/src/app/public/assets
within the image and even creates gzipped versions for us. This is important to notice because we’ll need to share these files from our prod_app
container over to our web server that we’re about to get up and running.
Setting up Nginx as our Web Server
There are a few things that we want Nginx to handle for us:
- Proxying for our Rails application. This will allow us to set up multiple backends when we need them.
- Serving static assets that have been precompiled.
For us to be able to do this without creating a custom Nginx image we will need to set up a volume to share our assets from our prod_app
container over to our webserver
container, and a volume to share our Nginx site configuration into the container also. Let’s lay out our docker-compose.prod.yml
file to let us accomplish this:
docker-compose.prod.yml
version: "2"
volumes:
assets:
external: false
configs:
external: false
db-data:
external: false
services:
webserver:
image: "nginx:1.11.8"
ports:
- "80:80"
- "443:443"
volumes:
- assets:/usr/share/nginx/html
- configs:/etc/nginx/conf.d
prod_db:
image: postgres
env_file: .env.prod
volumes:
- db-data:/var/lib/postgresql/db-data
prod_app:
build:
context: .
dockerfile: Dockerfile.prod
env_file: .env.prod
ports:
- "3000:3000"
volumes:
- assets:/usr/share/nginx/html
- configs:/etc/nginx/conf.d
depends_on:
- prod_db
- webserver
We’re also going to need to create a new file that will represent our site configuration in Nginx so that it will proxy to our Rails app.
site.conf
server {
listen 80;
# Properly serve assets
location ~ ^/(assets)/ {
root /usr/share/nginx/html;
gzip_static on;
expires max;
add_header Cache-Control public;
add_header ETag "";
}
# Proxy requests to rails app
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Set-Cookie;
proxy_pass http://prod_app:3000;
}
}
This brings us to a “gotcha” with volumes that we hadn’t previously looked at. Once a volume has been created and is connected to a container, it won’t be removed or overwritten by simply connecting another container. This will have an effect because our webserver
container will create our assets
and config
containers with its own default values and then when we create our prod_app
container we won’t be able to simply share our assets over because they volume already exists.
Serving Proper Assets on Update
Now that we have our site file up we’re going to need to rebuild our image so that it is packaged in, but before we do that we’re going to make sure that when we start our container in the production environment that we are ensuring the newest assets are in the right spot. We’ll do this by modifying our script/start
:
script/start
#!/bin/bash -e
if [[ -a /tmp/puma.pid ]]; then
rm /tmp/puma.pid
fi
if [[ $RAILS_ENV == "production" ]]; then
rake assets:precompile
mkdir -p /usr/share/nginx/html
cp -R public/* /usr/share/nginx/html/
mkdir -p /etc/nginx/conf.d/
cp site.conf /etc/nginx/conf.d/default.conf
fi
rails server -b 0.0.0.0 -P /tmp/puma.pid
Since we precompile assets when we build the image doing so again here doesn’t really cost us anything. We ensure that the html
directory exists so that we can copy the assets from the public
directory over. Finally, we move our site.conf
file into the configuration directory for Nginx. It’s safe for us to now flip the flag we set in our .env.prod
file that was making rails serve and compile assets on the fly.
POSTGRES_USER=meal_planner
POSTGRES_PASSWORD=dF1nu8xT6jBz01iXAfYDCmGdQO1IOc4EOgqVB703
POSTGRES_HOST=prod_db
RAILS_ENV=production
# RAILS_SERVE_STATIC_FILES=false
RAILS_LOG_TO_STDOUT=true
SECRET_KEY_BASE=7a475ef05d7f1100ae91c5e7ad6ab4706ce5d303e6bbb8da153d2accb7cb53fa5faeff3161b29232b3c08d6417bd05686094d04e22950a4767bc9236991570ad
Now we can rebuild our image and spin up our containers. We’ll start the webserver
container first so that we do get the Nginx static files as the first files in the assets
and config
volumes.
$ script/prod up -d webserver prod_db
$ script/prod up -d --build prod_app
If you head to localhost you should see the Nginx splash page, but that’s not the goal. The reason that you’re seeing this instead of our application is that we didn’t restart Nginx after we dropped off a new config for it to read. This is one occasion that I will actually say that using docker-compose restart
will work just fine for us:
$ script/prod restart webserver
Now if you head back to localhost you should see the meal plan application being served up (with assets) although we didn’t specify a port.
Recap
Today we vastly improved the deployment of our rails application by having a web server serve our static assets and proxy requests to our application so that we don’t need to connect to port 3000
directly. There’s still more improvement that can be done by digging deeper into Nginx, but for now, this works.