Using nginx to Customize Control of Your Hosted App
By: Michael Bogan
Tags: Cloud Architecture, DevOps, Heroku, Programming, Nginx

Middle-Tier Dynamic Port Binding

First, let’s look at dynamic port binding. To serve traffic to Heroku’s web dynos, you need access to an environment variable called PORT . This variable changes with each deployment and is not advertised before application start. This is a clear blocker for any service that does not have a way of binding to such a dynamic port.

Heroku does offer buildpacks that can automate the deployment and configuration of such a middle tier, but the solution for dynamic variables may not always be this easy. There are times when we might need to solve this or similar problems without the vendor’s help. So let’s look at how we might manually build a solution that can transform a statically configured application into a dynamically configured one, using BookStack.

BookStack is a self-proclaimed opinionated wiki system built in Laravel with a MySQL back end. BookStack has taken several design considerations out of the application deployer’s hands to simplify both its overall support architecture and to prevent the rabbit hole of wiki pages that are never found when they’re most needed.

To prep, we’ll need a few snippets gathered from BookStack and nginx’s official documentation to put together a Dockerfile and some basic scaffolding files. You can see the whole project here: https://github.com/Tokugero/bookstack-demo.

Let’s look at the Dockerfile:

Dockerfile

FROM debian:stable-slim

ENV PORT="80"

ENV APP_URL="http://localhost/"

ADD https://github.com/BookStackApp/BookStack/archive/release.zip /bookstack/

ADD https://getcomposer.org/installer /root/composer-setup.php

RUN apt-get update && \

apt-get install -y \

unzip \

php-cli \

php-mbstring \

php7.3-curl \

php7.3-dom \

php7.3-gd \

php7.3-mysql \

php7.3-tidy \

php7.3-xml \

php-fpm \

nginx && \

apt-get clean && \

rm -rf /var/lib/apt/lists/*

RUN unzip /bookstack/release.zip -d / && \

rm /bookstack/release.zip && \

php /root/composer-setup.php --install-dir=/usr/local/bin -- filename=composer && \

mkdir -p /var/lib/nginx && \

touch /run/nginx.pid && \

touch /var/log/php7.3-fpm.log

COPY config/bookstack.env /BookStack-release/.env

COPY config/nginx.conf /etc/nginx/nginx.conf

COPY scripts/run.sh /BookStack-release/run.sh

COPY config/nginx.htpasswd /BookStack-release/.htpasswd

RUN cd /BookStack-release && \

composer install --no-dev && \

chown -R www-data:www-data /BookStack-release/ && \

chown -R www-data:www-data /etc/nginx/ && \

chown -R www-data:www-data /var/lib/nginx/ && \

chown -R www-data:www-data /etc/php/7.3/fpm/ && \

chown www-data:www-data /run/nginx.pid && \

chown www-data:www-data /var/log/php7.3-fpm.log && \

chmod 600 .htpasswd

USER www-data

WORKDIR /BookStack-release/

ENTRYPOINT ["./run.sh"]

scripts/run.sh

#!/bin/bash

sed -i -e 's/$PORT/'"$PORT"'/g' /etc/nginx/nginx.conf

sed -i -e 's,APPURL,'${APP_URL}',g' /BookStack-release/.env

sed -i -e 's,listen = /run/php/php7.3-fpm.sock,listen = 127.0.0.1:9000,g' /etc/php/7.3/fpm/pool.d/www.conf

sed -i -e 's,pid = /run/php/php7.3-fpm.pid,pid = php7.3-fpm.pid,g' /etc/php/7.3/fpm/php-fpm.conf

cd /BookStack-release/ && \

echo yes | php artisan key:generate && \

echo yes | php artisan migrate

php-fpm7.3 & \

nginx -g 'daemon off;'

config/nginx.conf

worker_processes 4;

error_log /dev/stderr;

user www-data;

include /etc/nginx/modules/*.conf;

events {

worker_connections 4096; ## Default: 1024

} http {

include /etc/nginx/fastcgi.conf;

include /etc/nginx/mime.types;

index index.html index.htm index.php;

default_type application/octet-stream;

access_log /dev/stdout;

sendfile on;

tcp_nopush on;

server {

#This is updated via sed in ./scripts/run.sh at runtimelisten $PORT;

server_name _;

root /BookStack-release/public;

client_max_body_size 0;

location / {

index index.php;

try_files $uri $uri/ /index.php?$query_string;

}

location ~ \.php$ {

fastcgi_split_path_info ^(.+?\.php)(/.*)$;

if (!-f $document_root$fastcgi_script_name) {

return 404;

}

# Mitigate https://httpoxy.org/ vulnerabilities

fastcgi_param HTTP_PROXY "";

fastcgi_pass 127.0.0.1:9000;

fastcgi_index index.php;

# include the fastcgi_param setting

include fastcgi_params;

# SCRIPT_FILENAME parameter is used for PHP FPM determining

# the script name. If it is not set in fastcgi_params file,

# i.e. /etc/nginx/fastcgi_params or in the parent contexts,

# please comment off following line:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

}

}

}

config/bookstack.env

APP_KEY=replaceme

APP_URL=APPURL

Now this seems like a lot, but let’s break down some of the more important bits that make up the core functionality.

The Dockerfile has many lines that are primarily for installing the application itself. These were found on the official BookStack documentation and are used to manually install their service along with a few extra packages. The goal is to make the environment fit their application. To help make the service more dynamic, there are three specific lines included:



ENV PORT="80"

ENV APP_URL="

... ...ENV PORT="80"ENV APP_URL=" http://localhost/ ... ENTRYPOINT ["./run.sh"]

This sets up a default environment variable and calls an arbitrary shell script to replace the nginx configuration file values with environment values at runtime. With this, we can customize this application however we like. Instead of hard-coded variables to run this service, we can now instantiate the service locally:

docker run -it -d -e APP_URL=http://localhost:9876 -e PORT=8080 -p 9876:8080 bookstack-demo

Notice how we can now declare the port at runtime without any special configuration of the application itself — the core requirement to exposing a service on Heroku’s web dynos. To pass these in, we simply utilized sed to substitute the environment variables with the hardcoded values at run time.

sed -i -e 's/$PORT/'"$PORT"'/g' /etc/nginx/nginx.conf;

sed -i -e 's,APPURL,'${APP_URL}',g' /BookStack-release/.env;

In our run.sh script that initiates the container primary command, we can use our stream editor/sed to replace the predefined variables in nginx’s configuration file as well as in the application’s dedicated environment file. When we do this before our nginx initialization, we guarantee that the application is starting with the port that Heroku is defining after the container has been brought to life.

And one final deployment command: