How-To: Serve static files with nginx

Serve static files with nginx while proxying the rest of requests to your app in a staging environment.

So, your simple tech stack includes nginx that serves static files, and your app which serves the other web requests. And now you want to replicate the same for your staging environment on Dockup. Well, you are on the right page.

First things first, in this example, I'll use rails as my web app. Also, here's the overview of how it'll work.

  1. We use a multi stage Dockerfile to create two separate custom images, one for nginx and the other for rails.

  2. We use the Environment Variables Substitutions to get the hostname of the rails web app in nginx container

  3. We use a custom shell script inside our nginx image to update the nginx config with the rails app's hostname.

Custom images. So, we'll have our own custom nginx image. Why we need this? Well, we want to dynamically update the nginx config with the hostname of rails app. Also, we use a multi stage Dockerfile because, a. it's easy to manage, b. it creates less artifacts in newer images and c. we can build only particular stages. So, in our Dockerfile, the first stage compiles the assets, the second stage prepares the nginx container, and the third prepares the rails container. Here's the sample Dockerfile that does exactly this.

Dockerfile
FROM ruby:2.5.1 as assets
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - && \
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && \
apt-get install -y yarn
RUN mkdir /app
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --help
RUN yarn --version
RUN yarn install --frozen-lockfile
COPY Gemfile Gemfile.lock ./
RUN bundle install --frozen --without development test --path=vendor
# master key can be passed as env var to docker build
COPY . .
RUN echo "2c498ac13a201b498910d0a167d1fb52" > config/master.key
RUN bundle exec rake assets:precompile RAILS_ENV=staging
# ===================================================================================
FROM nginx as nginx
COPY --from=assets /app/public /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY run-nginx /app/run-nginx
CMD /app/run-nginx
# ===================================================================================
FROM ruby:2.5.1 as app
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - && \
apt-get install -y nodejs
RUN mkdir /app
WORKDIR /app
# master key can be passed as env var to docker build
COPY . .
RUN echo "2c498ac13a201b498910d0a167d1fb52" > config/master.key
COPY --from=assets /app/vendor vendor
COPY --from=assets /app/public public
RUN bundle install --frozen --without development test --path=vendor
EXPOSE 3000
CMD ./bin/dockup

Environment Variables Substitutions. So now we need the hostname of our app inside nginx container. And thankfully, Dockup has this neat feature which allows you to do exactly that. You can set a special environment variable in nginx container of your blueprint. It'll look something like,

Using Environment Variable Substitutions feature to get hostname of rails app

We set the RAILS_HOSTNAME to have the hostname of the rails app and RAILS_PORT to have the port of our server.

Custom shell script. We now have the hostname of the rails app inside the nginx contianer, we only need to update the nginx config with this and restart the nginx server. Here's a sample script that does that.

run-nginx
#!/bin/sh
set -e
set -x
sed -i -e "s/\$RAILS_HOSTNAME/$RAILS_HOSTNAME/g" /etc/nginx/conf.d/default.conf;
sed -i -e "s/\$RAILS_PORT/$RAILS_PORT/g" /etc/nginx/conf.d/default.conf;
nginx -g "daemon off;"

And here's the sample nginx config.

nginx.conf
server {
listen 80;
server_name localhost;
# add extenstion for any other static files you want to be served by nginx
location ~ \.(js|css|ico|ttf|otf|woff|svg) {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://$RAILS_HOSTNAME:$RAILS_PORT;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

So there you have it. Now you can have nginx serving static files for you while proxying the rest of the request to your app.

P.S. Here's the code for the entire setup https://github.com/dockup/examples/tree/master/rails5-nginx