{"id":218,"date":"2018-10-16T03:46:53","date_gmt":"2018-10-16T03:46:53","guid":{"rendered":"http:\/\/www.appservgrid.com\/paw93\/index.php\/2018\/10\/16\/deploying-configurable-frontend-web-application-containers\/"},"modified":"2018-10-16T03:46:53","modified_gmt":"2018-10-16T03:46:53","slug":"deploying-configurable-frontend-web-application-containers","status":"publish","type":"post","link":"https:\/\/www.appservgrid.com\/paw93\/index.php\/2018\/10\/16\/deploying-configurable-frontend-web-application-containers\/","title":{"rendered":"Deploying configurable frontend web application containers"},"content":{"rendered":"<p> Sep 19, 2018<\/p>\n<p>\n <img loading=\"lazy\" decoding=\"async\" alt=\"Alternative Text\" height=\"150\" src=\"https:\/\/container-solutions.com\/content\/uploads\/2018\/09\/IMG_3965-150x150.jpg\" width=\"150\" \/> by <a href=\"https:\/\/container-solutions.com\/author\/jose\/\">Jos\u00e9 Moreira<\/a> <\/p>\n<p>The approach for deploying a containerised application typically involves building a Docker container image from a Dockerfile once and deploying that same image across several deployment environments (development, staging, testing, production).<\/p>\n<p>If following security best practices, each deployment environment will require a different configuration (data storage authentication credentials, external API URLs) and the configuration is injected into the application inside the container through environment variables or configuration files. Our <a href=\"https:\/\/container-solutions.com\/author\/hamish\/\" target=\"_blank\">Hamish Hutchings<\/a> takes a deeper look at 12-factor app <a href=\"https:\/\/container-solutions.com\/golang-configuration-in-12-factor-applications\/\" target=\"_blank\">in this blog post<\/a>. Also, the possible configuration profiles might not be predetermined, if for example, the web application should be ready to be deployed both on public or private cloud (client premises), as it is also common for several configuration profiles to be added to source code and the required profile to be loaded at build time.<\/p>\n<p>The structure of a web application project typically contains a \u2018src\u2019 directory with source code and executing npm run-script build triggers the Webpack asset build pipeline. Final asset bundles (HTML, JS, CSS, graphics, and fonts) are written to a dist directory and contents are either uploaded to a CDN or served with a web server (NGINX, Apache, Caddy, etc).<\/p>\n<p>For context in this article, let\u2019s assume the web application is a single-page frontend application which connects to a backend REST API to fetch data, for which the API endpoint will change across deployment environments. The backend API endpoint should, therefore, be fully configurable and configuration approach should support both server deployment and local development and assets are served by NGINX.<\/p>\n<p>Deploying client-side web application containers requires a different configuration strategy compared to server-side applications containers. Given the nature of client-side web applications, there is no native executable that can read environment variables or configuration files in runtime, the runtime is the client-side web browser and configuration has to be hard-coded in the Javascript source code either by hard-coding values during the asset build phase or hard-coding rules (a rule would be to deduce current environment based on the domain name, ex: \u2018staging.app.com\u2019).<\/p>\n<p>There is one OS process which is relevant to configuration in which reading values from environment values is useful, which is the asset build Node JS process and this is helpful for configuring the app for local development with local development auto reload.<\/p>\n<p>For the configuration of the webapp across several different environments, there are a few solutions:<\/p>\n<ol>\n<li>Rebuild the Webpack assets on container start during each deployment with the proper configuration on the destination server node(s):\n<ul>\n<li>Adds to deployment time. Depending on deployment rate and size of the project, the deployment time overhead might be considerable.<\/li>\n<li>Is prone to build failures at end of deployment pipeline even if the image build has been tested before.<\/li>\n<li>Build phase can fail for example due to network conditions although this can probably be minimised by building on top of a docker image that already has all the dependencies installed.<\/li>\n<li>Might affect rollback speed<\/li>\n<\/ul>\n<\/li>\n<li>Build one image per environment (again with hardcoded configuration):\n<ul>\n<li>Similar solutions (and downsides) of solution #1 except that it adds clutter to the docker registry\/daemon.<\/li>\n<\/ul>\n<\/li>\n<li>Build image once and rewrite configuration bits only during each deployment to target environment:\n<ul>\n<li>Image is built once and ran everywhere. Aligns with the configuration pattern of other types of applications which is good for normalisation<\/li>\n<li>Scripts that rewrite configuration inside the container can be prone to failure too but they are testable.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p>I believe solution #1 is viable and, in some cases, simpler and probably required if the root path where the web application is hosted needs to change dynamically, ex.: from \u2018\/\u2019 to \u2018\/app\u2019, as build pipelines can hardcode the base path of fonts and other graphics in CSS files with the root path, which is a lot harder to change post build.<\/p>\n<p>Solution #3 is the approach I have been implementing for the projects where I have been responsible for containerising web applications (both at my current and previous roles), which is the solution also implemented by my friend Israel and for which he helped me out implementing the first time around and the approach that will be described in this article.<\/p>\n<h2>Application-level configuration<\/h2>\n<p>Although it has a few moving parts, the plan for solution #3 is rather straightforward:<\/p>\n<p><a href=\"http:\/\/container-solutions.com\/content\/uploads\/2018\/09\/configurable-frontend-application-architecture.png\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" height=\"447\" src=\"http:\/\/container-solutions.com\/content\/uploads\/2018\/09\/configurable-frontend-application-architecture.png\" width=\"1188\" \/><\/a><\/p>\n<p>For code samples, I will utilise my <a href=\"https:\/\/github.com\/zemanel\/doom-client\/tree\/vue-webpack-refactor\" target=\"_blank\">fork<\/a> of Adam Sandor micro-service Doom web client, which I have been refactoring to follow this technique, which is a Vue.js application. The web client communicates with two micro-services through HTTP APIs, the state and the engine, endpoints which I would like to be configurable without rebuilding the assets.<\/p>\n<p>Single Page Applications (SPA) have a single \u201cindex.html\u201d as a entry point to the app and during deployment. meta tags with optional configuration defaults are added to the markup from which application can read configuration values. script tags would also work but I found meta tags simple enough for key value pairs.<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<\/p>\n<p>1<\/p>\n<p>2<\/p>\n<p>3<\/p>\n<p>4<\/p>\n<p>5<\/p>\n<p>6<\/p>\n<p>7<\/p>\n<p>8<\/p>\n<p>9<\/p>\n<p>10<\/p>\n<p>11<\/p>\n<p>12<\/p>\n<p>13<\/p>\n<p>14<\/p>\n<p>15<\/p>\n<p>16<\/p>\n<p>17<\/p>\n<p>18<\/p>\n<p>19<\/p>\n<p>20<\/p>\n<p>21<\/p>\n<p>&#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>&lt;!DOCTYPE html&gt;<\/p>\n<p>&lt;html lang=&#8221;en&#8221;&gt;<\/p>\n<p> &lt;head&gt;<\/p>\n<p> &lt;meta charset=&#8221;utf-8&#8243;&gt;<\/p>\n<p> &lt;meta http-equiv=&#8221;X-UA-Compatible&#8221; content=&#8221;IE=edge&#8221;&gt;<\/p>\n<p> &lt;meta name=&#8221;viewport&#8221; content=&#8221;width=device-width,initial-scale=1.0&#8243;&gt;<\/p>\n<p> &lt;meta property=&#8221;DOOM_STATE_SERVICE_URL&#8221; content=&#8221;http:\/\/localhost:8081\/&#8221; \/&gt;<\/p>\n<p> &lt;meta property=&#8221;DOOM_ENGINE_SERVICE_URL&#8221; content=&#8221;http:\/\/localhost:8082\/&#8221; \/&gt;<\/p>\n<p> &lt;link rel=&#8221;icon&#8221; href=&#8221;.\/favicon.ico&#8221;&gt;<\/p>\n<p> &lt;title&gt;frontend&lt;\/title&gt;<\/p>\n<p> &lt;\/head&gt;<\/p>\n<p> &lt;body&gt;<\/p>\n<p> &lt;noscript&gt;<\/p>\n<p> &lt;strong&gt;We&#8217;re sorry but frontend doesn&#8217;t work properly without JavaScript enabled. Please enable it to continue.&lt;\/strong&gt;<\/p>\n<p> &lt;\/noscript&gt;<\/p>\n<p> &lt;div id=&#8221;app&#8221;&gt;&lt;\/div&gt;<\/p>\n<p> &lt;!&#8211; built files will be auto injected &#8211;&gt;<\/p>\n<p> &lt;\/body&gt;<\/p>\n<p>&lt;\/html&gt;<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>For reading configuration values from meta tags (and other sources), I wrote a simple Javascript module (\u201c\/src\/config.loader.js\u201d):<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<\/p>\n<p>1<\/p>\n<p>2<\/p>\n<p>3<\/p>\n<p>4<\/p>\n<p>5<\/p>\n<p>6<\/p>\n<p>7<\/p>\n<p>8<\/p>\n<p>9<\/p>\n<p>10<\/p>\n<p>11<\/p>\n<p>12<\/p>\n<p>13<\/p>\n<p>14<\/p>\n<p>15<\/p>\n<p>16<\/p>\n<p>17<\/p>\n<p>18<\/p>\n<p>19<\/p>\n<p>20<\/p>\n<p>21<\/p>\n<p>22<\/p>\n<p>23<\/p>\n<p>24<\/p>\n<p>25<\/p>\n<p>26<\/p>\n<p>27<\/p>\n<p>28<\/p>\n<p>29<\/p>\n<p>30<\/p>\n<p>31<\/p>\n<p>&#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>\/**<\/p>\n<p> * Get config value with precedence:<\/p>\n<p> * &#8211; check `process.env`<\/p>\n<p> * &#8211; check current web page meta tags<\/p>\n<p> * @param key Configuration key name<\/p>\n<p> *\/<\/p>\n<p>function getConfigValue (key) {<\/p>\n<p> let value = null<\/p>\n<p> if (process.env &amp;&amp; process.env[`$`] !== undefined) {<\/p>\n<p> \/\/ get env var value<\/p>\n<p> value = process.env[`$`]<\/p>\n<p> } else {<\/p>\n<p> \/\/ get value from meta tag<\/p>\n<p> return getMetaValue(key)<\/p>\n<p> }<\/p>\n<p> return value<\/p>\n<p>}<\/p>\n<p>\/**<\/p>\n<p> * Get value from HTML meta tag<\/p>\n<p> *\/<\/p>\n<p>function getMetaValue (key) {<\/p>\n<p> let value = null<\/p>\n<p> const node = document.querySelector(`meta[property=$]`)<\/p>\n<p> if (node !== null) {<\/p>\n<p> value = node.content<\/p>\n<p> }<\/p>\n<p> return value<\/p>\n<p>}<\/p>\n<p>export default { getConfigValue, getMetaValue }<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>This module will read configuration \u201ckeys\u201d by looking them up in the available environment variables (\u201cprocess.env\u201d) first, so that configuration can be overridden with environment variables when developing locally (webpack dev server) and then the current document meta tags.<\/p>\n<p>I also abstracted the configuration layer by adding a \u201csrc\/config\/index.js\u201d that exports an object with the proper values:<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<br \/>\n &#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>import loader from &#8216;.\/loader&#8217;<\/p>\n<p>export default {<\/p>\n<p> DOOM_STATE_SERVICE_URL: loader.getConfigValue(&#8216;DOOM_STATE_SERVICE_URL&#8217;),<\/p>\n<p> DOOM_ENGINE_SERVICE_URL: loader.getConfigValue(&#8216;DOOM_ENGINE_SERVICE_URL&#8217;)<\/p>\n<p>}<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>which can then be utilised in the main application by importing the \u201csrc\/config\u201d module and accessing the configuration keys transparently:<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<br \/>\n &#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>import config from &#8216;.\/config&#8217;<\/p>\n<p>console.log(config.DOOM_ENGINE_SERVICE_URL)<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>There is some room for improvement in the current code as it not DRY (list of required configuration variables is duplicated in several places in the project) and I\u2019ve considered writing a simple Javascript package to simplify this approach as I\u2019m not aware if something already exists. Writing the Docker &amp; Docker Compose files The Dockerfile for the SPA adds source-code to the container to the \u2018\/app\u2019 directory, installs dependencies and runs a production webpack build (\u201cNODE_ENV=production\u201d). Assets bundles are written to the \u201c\/app\/dist\u201d directory of the image:<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<br \/>\n &#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>FROM node:8.11.4-jessie<\/p>\n<p>RUN mkdir \/app<\/p>\n<p>WORKDIR \/app<\/p>\n<p>COPY package.json .<\/p>\n<p>RUN npm install<\/p>\n<p>COPY . .<\/p>\n<p>ENV NODE_ENV production<\/p>\n<p>RUN npm run build<\/p>\n<p>CMD npm run dev<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>The docker image contains a Node.js script (\u201c\/app\/bin\/rewrite-config.js\u201d) which copies \u201c\/app\/dist\u201d assets to another target directory before rewriting the configuration. Assets will be served by NGINX and therefore copied to a directory that NGINX can serve, in this case, a shared (persistent) volume. Source and destination directories can be defined through container environment variables:<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<\/p>\n<p>1<\/p>\n<p>2<\/p>\n<p>3<\/p>\n<p>4<\/p>\n<p>5<\/p>\n<p>6<\/p>\n<p>7<\/p>\n<p>8<\/p>\n<p>9<\/p>\n<p>10<\/p>\n<p>11<\/p>\n<p>12<\/p>\n<p>13<\/p>\n<p>14<\/p>\n<p>15<\/p>\n<p>16<\/p>\n<p>17<\/p>\n<p>18<\/p>\n<p>19<\/p>\n<p>20<\/p>\n<p>21<\/p>\n<p>22<\/p>\n<p>23<\/p>\n<p>24<\/p>\n<p>25<\/p>\n<p>26<\/p>\n<p>27<\/p>\n<p>28<\/p>\n<p>29<\/p>\n<p>30<\/p>\n<p>31<\/p>\n<p>32<\/p>\n<p>33<\/p>\n<p>34<\/p>\n<p>35<\/p>\n<p>36<\/p>\n<p>37<\/p>\n<p>38<\/p>\n<p>39<\/p>\n<p>40<\/p>\n<p>41<\/p>\n<p>42<\/p>\n<p>43<\/p>\n<p>44<\/p>\n<p>45<\/p>\n<p>46<\/p>\n<p>47<\/p>\n<p>48<\/p>\n<p>49<\/p>\n<p>50<\/p>\n<p>51<\/p>\n<p>52<\/p>\n<p>53<\/p>\n<p>54<\/p>\n<p>55<\/p>\n<p>56<\/p>\n<p>57<\/p>\n<p>58<\/p>\n<p>59<\/p>\n<p>&#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>#!\/usr\/bin\/env node<\/p>\n<p>const cheerio = require(&#8216;cheerio&#8217;)<\/p>\n<p>const copy = require(&#8216;recursive-copy&#8217;)<\/p>\n<p>const fs = require(&#8216;fs&#8217;)<\/p>\n<p>const rimraf = require(&#8216;rimraf&#8217;)<\/p>\n<p>const DIST_DIR = process.env.DIST_DIR<\/p>\n<p>const WWW_DIR = process.env.WWW_DIR<\/p>\n<p>const DOOM_STATE_SERVICE_URL = process.env.DOOM_STATE_SERVICE_URL<\/p>\n<p>const DOOM_ENGINE_SERVICE_URL = process.env.DOOM_ENGINE_SERVICE_URL<\/p>\n<p>\/\/ &#8211; Delete existing files from public directory<\/p>\n<p>\/\/ &#8211; Copy `dist` assets to public directory<\/p>\n<p>\/\/ &#8211; Rewrite config meta tags on public directory `index.html`<\/p>\n<p>rimraf(WWW_DIR + &#8216;\/*&#8217;, {}, function() {<\/p>\n<p> copy(`$`, `$`, , function(error, results) {<\/p>\n<p> if (error) {<\/p>\n<p> console.error(&#8216;Copy failed: &#8216; + error);<\/p>\n<p> } else {<\/p>\n<p> console.info(&#8216;Copied &#8216; + results.length + &#8216; files&#8217;);<\/p>\n<p> rewriteIndexHTML(`$\/index.html`, {<\/p>\n<p> DOOM_STATE_SERVICE_URL: DOOM_STATE_SERVICE_URL,<\/p>\n<p> DOOM_ENGINE_SERVICE_URL: DOOM_ENGINE_SERVICE_URL<\/p>\n<p> })<\/p>\n<p> }<\/p>\n<p> });<\/p>\n<p>})<\/p>\n<p>\/**<\/p>\n<p>* Rewrite meta tag config values in &#8220;index.html&#8221;.<\/p>\n<p>* @param file<\/p>\n<p>* @param values<\/p>\n<p>*\/<\/p>\n<p>function rewriteIndexHTML(file, values) {<\/p>\n<p> console.info(`Reading &#8216;$&#8217;`)<\/p>\n<p> fs.readFile(file, &#8216;utf8&#8217;, function (error, data) {<\/p>\n<p> if (!error) {<\/p>\n<p> const $ = cheerio.load(data)<\/p>\n<p> console.info(`Rewriting values &#8216;$&#8217;`)<\/p>\n<p> for (let [key, value] of Object.entries(values)) {<\/p>\n<p> console.log(key, value);<\/p>\n<p> $(`[property=$]`).attr(&#8220;content&#8221;, value);<\/p>\n<p> }<\/p>\n<p> fs.writeFile(file, $.html(), function (error) {<\/p>\n<p> if (!error) {<\/p>\n<p> console.info(`Wrote &#8216;$&#8217;`)<\/p>\n<p> } else {<\/p>\n<p> console.error(error)<\/p>\n<p> }<\/p>\n<p> });<\/p>\n<p> } else {<\/p>\n<p> console.error(error)<\/p>\n<p> }<\/p>\n<p> });<\/p>\n<p>}<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>The script utilises CheerioJS to read the \u201cindex.html\u201d into memory, replaces values of meta tags according to environment variables and overwrites \u201cindex.html\u201d. Although \u201csed\u201d would have been sufficient for search &amp; replace, I chose CherioJS as a more reliable solution that also allows expanding into more complex solutions like script injections.<\/p>\n<h2>Deployment with Kubernetes<\/h2>\n<p>Let\u2019s jump into the Kubernetes Deployment manifest:<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<\/p>\n<p>1<\/p>\n<p>2<\/p>\n<p>3<\/p>\n<p>4<\/p>\n<p>5<\/p>\n<p>6<\/p>\n<p>7<\/p>\n<p>8<\/p>\n<p>9<\/p>\n<p>10<\/p>\n<p>11<\/p>\n<p>12<\/p>\n<p>13<\/p>\n<p>14<\/p>\n<p>15<\/p>\n<p>16<\/p>\n<p>17<\/p>\n<p>18<\/p>\n<p>19<\/p>\n<p>20<\/p>\n<p>21<\/p>\n<p>22<\/p>\n<p>23<\/p>\n<p>24<\/p>\n<p>25<\/p>\n<p>26<\/p>\n<p>27<\/p>\n<p>28<\/p>\n<p>29<\/p>\n<p>30<\/p>\n<p>31<\/p>\n<p>32<\/p>\n<p>33<\/p>\n<p>34<\/p>\n<p>35<\/p>\n<p>36<\/p>\n<p>37<\/p>\n<p>38<\/p>\n<p>39<\/p>\n<p>40<\/p>\n<p>41<\/p>\n<p>42<\/p>\n<p>43<\/p>\n<p>44<\/p>\n<p>45<\/p>\n<p>46<\/p>\n<p>47<\/p>\n<p>48<\/p>\n<p>49<\/p>\n<p>50<\/p>\n<p>&#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p>&#8212;<\/p>\n<p># Source: doom-client\/templates\/deployment.yaml<\/p>\n<p>apiVersion: apps\/v1beta2<\/p>\n<p>kind: Deployment<\/p>\n<p>metadata:<\/p>\n<p> name: doom-client<\/p>\n<p> labels:<\/p>\n<p> name: doom-client<\/p>\n<p>spec:<\/p>\n<p> replicas: 1<\/p>\n<p> selector:<\/p>\n<p> matchLabels:<\/p>\n<p> name: doom-client<\/p>\n<p> template:<\/p>\n<p> metadata:<\/p>\n<p> labels:<\/p>\n<p> name: doom-client<\/p>\n<p> spec:<\/p>\n<p> initContainers:<\/p>\n<p> &#8211; name: doom-client<\/p>\n<p> image: &#8220;doom-client:latest&#8221;<\/p>\n<p> command: [&#8220;\/app\/bin\/rewrite-config.js&#8221;]<\/p>\n<p> imagePullPolicy: IfNotPresent<\/p>\n<p> env:<\/p>\n<p> &#8211; name: DIST_DIR<\/p>\n<p> value: &#8220;\/app\/dist&#8221;<\/p>\n<p> &#8211; name: WWW_DIR<\/p>\n<p> value: &#8220;\/tmp\/www&#8221;<\/p>\n<p> &#8211; name: DOOM_ENGINE_SERVICE_URL<\/p>\n<p> value: &#8220;http:\/\/localhost:8081\/&#8221;<\/p>\n<p> &#8211; name: DOOM_STATE_SERVICE_URL<\/p>\n<p> value: &#8220;http:\/\/localhost:8082\/&#8221;<\/p>\n<p> volumeMounts:<\/p>\n<p> &#8211; name: www-data<\/p>\n<p> mountPath: \/tmp\/www<\/p>\n<p> containers:<\/p>\n<p> &#8211; name: nginx<\/p>\n<p> image: nginx:1.14<\/p>\n<p> imagePullPolicy: IfNotPresent<\/p>\n<p> volumeMounts:<\/p>\n<p> &#8211; name: www-data<\/p>\n<p> mountPath: \/usr\/share\/nginx\/html<\/p>\n<p> &#8211; name: doom-client-nginx-vol<\/p>\n<p> mountPath: \/etc\/nginx\/conf.d<\/p>\n<p> volumes:<\/p>\n<p> &#8211; name: www-data<\/p>\n<p> emptyDir: {}<\/p>\n<p> &#8211; name: doom-client-nginx-vol<\/p>\n<p> configMap:<\/p>\n<p> name: doom-client-nginx<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>The Deployment manifest defines an \u201cinitContainer\u201d which executes the \u201crewrite-config.js\u201d Node.js script to prepare and update the shared storage volume with the asset bundles. It also defines an NGINX container for serving our static assets. Finally, it also creates a shared Persistent Volume which is mounted on both of the above containers. In the NGINX container the mount point is \u201c\/var\/www\/share\/html\u201d but on the frontend container \u201c\/tmp\/www\u201d for avoiding creating extra directories. \u201c\/tmp\/www\u201d will be the directory where the Node.js script will copy asset bundles to and rewrite the \u201cindex.html\u201d. Local development with Docker Compose<\/p>\n<p>The final piece of our puzzle is the local Docker Compose development environment. I\u2019ve included several services that allow both developing the web application with the development server and testing the application when serving production static assets through NGINX. It is perfectly possible to separate these services into several YAML files (\u201cdocker-compose.yaml\u201d, \u201cdocker-compose.dev.yaml\u201d and \u201cdocker-compose.prod.yaml\u201d) and do some composition but I\u2019ve added a single file for the sake of simplicity.<\/p>\n<p>Apart from the \u201cdoom-state\u201d and \u201cdoom-engine\u201d services which are our backend APIs, the \u201cui\u201d service starts the webpack development server with \u201cnpm run dev\u201d and the \u201cui-deployment\u201d service, which runs a container based on the same Dockerfile, runs the configuration deployment script. The \u201cnginx\u201d service serves static assets from a persistent volume (\u201cwww-data\u201d) which is also mounted on the \u201cui-deployment\u201d script.<\/p>\n<p>&#xD;<br \/>\n&#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<br \/>\n &#xD;<\/p>\n<table>\n<tr>\n<td>&#xD;<\/p>\n<p>1<\/p>\n<p>2<\/p>\n<p>3<\/p>\n<p>4<\/p>\n<p>5<\/p>\n<p>6<\/p>\n<p>7<\/p>\n<p>8<\/p>\n<p>9<\/p>\n<p>10<\/p>\n<p>11<\/p>\n<p>12<\/p>\n<p>13<\/p>\n<p>14<\/p>\n<p>15<\/p>\n<p>16<\/p>\n<p>17<\/p>\n<p>18<\/p>\n<p>19<\/p>\n<p>20<\/p>\n<p>21<\/p>\n<p>22<\/p>\n<p>23<\/p>\n<p>24<\/p>\n<p>25<\/p>\n<p>26<\/p>\n<p>27<\/p>\n<p>28<\/p>\n<p>29<\/p>\n<p>30<\/p>\n<p>31<\/p>\n<p>32<\/p>\n<p>33<\/p>\n<p>34<\/p>\n<p>35<\/p>\n<p>36<\/p>\n<p>37<\/p>\n<p>38<\/p>\n<p>39<\/p>\n<p>40<\/p>\n<p>41<\/p>\n<p>42<\/p>\n<p>43<\/p>\n<p>44<\/p>\n<p>45<\/p>\n<p>46<\/p>\n<p>47<\/p>\n<p>48<\/p>\n<p>49<\/p>\n<p>50<\/p>\n<p>51<\/p>\n<p>52<\/p>\n<p>53<\/p>\n<p>54<\/p>\n<p>55<\/p>\n<p>56<\/p>\n<p>57<\/p>\n<p>58<\/p>\n<p>59<\/p>\n<p>60<\/p>\n<p>61<\/p>\n<p>62<\/p>\n<p>63<\/p>\n<p>64<\/p>\n<p>&#xD;\n <\/td>\n<p>&#xD;<\/p>\n<td>\n<p># docker-compose.yaml<\/p>\n<p>version: &#8216;3&#8217;<\/p>\n<p>services:<\/p>\n<p> ui:<\/p>\n<p> build: .<\/p>\n<p> command: [&#8220;npm&#8221;, &#8220;run&#8221;, &#8220;dev&#8221;, ]<\/p>\n<p> ports:<\/p>\n<p> &#8211; &#8220;8080:8080&#8221;<\/p>\n<p> environment:<\/p>\n<p> &#8211; HOST=0.0.0.0<\/p>\n<p> &#8211; PORT=8080<\/p>\n<p> &#8211; NODE_ENV=development<\/p>\n<p> &#8211; DOOM_ENGINE_SERVICE_URL=http:\/\/localhost:8081\/<\/p>\n<p> &#8211; DOOM_STATE_SERVICE_URL=http:\/\/localhost:8082\/<\/p>\n<p> volumes:<\/p>\n<p> &#8211; .:\/app<\/p>\n<p> # bind volume inside container for source mount not shadow image dirs<\/p>\n<p> &#8211; \/app\/node_modules<\/p>\n<p> &#8211; \/app\/dist<\/p>\n<p> doom-engine:<\/p>\n<p> image: microservice-doom\/doom-engine:latest<\/p>\n<p> environment:<\/p>\n<p> &#8211; DOOM_STATE_SERVICE_URL=http:\/\/doom-state:8080\/<\/p>\n<p> &#8211; DOOM_STATE_SERVICE_PASSWORD=enginepwd<\/p>\n<p> ports:<\/p>\n<p> &#8211; &#8220;8081:8080&#8221;<\/p>\n<p> doom-state:<\/p>\n<p> image: microservice-doom\/doom-state:latest<\/p>\n<p> ports:<\/p>\n<p> &#8211; &#8220;8082:8080&#8221;<\/p>\n<p># run production deployment script<\/p>\n<p> ui-deployment:<\/p>\n<p> build: .<\/p>\n<p> command: [&#8220;\/app\/bin\/rewrite-config.js&#8221;]<\/p>\n<p> environment:<\/p>\n<p> &#8211; NODE_ENV=production<\/p>\n<p> &#8211; DIST_DIR=\/app\/dist<\/p>\n<p> &#8211; WWW_DIR=\/tmp\/www<\/p>\n<p> &#8211; DOOM_ENGINE_SERVICE_URL=http:\/\/localhost:8081\/<\/p>\n<p> &#8211; DOOM_STATE_SERVICE_URL=http:\/\/localhost:8082\/<\/p>\n<p> volumes:<\/p>\n<p> &#8211; .:\/app<\/p>\n<p> # bind volume inside container for source mount not shadow image dirs<\/p>\n<p> &#8211; \/app\/node_modules<\/p>\n<p> &#8211; \/app\/dist<\/p>\n<p> # shared NGINX static files dir<\/p>\n<p> &#8211; www-data:\/tmp\/www<\/p>\n<p> depends_on:<\/p>\n<p> &#8211; nginx<\/p>\n<p> # serve docker image production build with nginx<\/p>\n<p> nginx:<\/p>\n<p> image: nginx:1.14<\/p>\n<p> ports:<\/p>\n<p> &#8211; &#8220;8090:80&#8221;<\/p>\n<p> volumes:<\/p>\n<p> &#8211; www-data:\/usr\/share\/nginx\/html<\/p>\n<p>volumes:<\/p>\n<p> www-data:<\/p>\n<\/td>\n<p>&#xD;<br \/>\n <\/tr>\n<\/table>\n<p>&#xD;<br \/>\n &#xD;<br \/>\n&#xD;<\/p>\n<p>Since the webpack dev server is a long running process which also hot-reloads the app on source code changes, the Node.js config module will yield configuration from environment variables, based on the precedence I created. Also, although source code changes can trigger client-side updates without restarts (hot reload), it will not update the production build, which has to be manual but straightforward with a $ docker-compose build &amp;&amp; docker-compose up.<\/p>\n<p>Summarizing, although there are a few improvements points, including on the source code I wrote for this implementation, this setup has been working pretty well for the last few projects and is flexible enough to also support deployments to CDNs, which is as simple as adding a step for pushing assets to the cloud instead of a shared volume with NGINX.<\/p>\n<p>If you have any comments feel free to get in touch on <a href=\"https:\/\/twitter.com\/zemanel\" target=\"_blank\">Twitter<\/a> or comment under the article.<\/p>\n<p>Download our free whitepaper, Kubernetes: Crossing the Chasm below.<\/p>\n<p><a href=\"https:\/\/cta-redirect.hubspot.com\/cta\/redirect\/2252258\/79038edb-11db-4edf-a310-cfe87e3d3670\" target=\"_blank\"><img loading=\"lazy\" decoding=\"async\" alt=\"Download Whitepaper\" height=\"250\" src=\"https:\/\/no-cache.hubspot.com\/cta\/default\/2252258\/79038edb-11db-4edf-a310-cfe87e3d3670.png\" width=\"600\" \/><\/a><\/p>\n<p> <a href=\"https:\/\/container-solutions.com\/deploying-configurable-frontend-web-application-containers\/\" target=\"_blank\">Source<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sep 19, 2018 by Jos\u00e9 Moreira The approach for deploying a containerised application typically involves building a Docker container image from a Dockerfile once and deploying that same image across several deployment environments (development, staging, testing, production). If following security best practices, each deployment environment will require a different configuration (data storage authentication credentials, external &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/www.appservgrid.com\/paw93\/index.php\/2018\/10\/16\/deploying-configurable-frontend-web-application-containers\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Deploying configurable frontend web application containers&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-218","post","type-post","status-publish","format-standard","hentry","category-kubernetes"],"_links":{"self":[{"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/posts\/218","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/comments?post=218"}],"version-history":[{"count":0,"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/posts\/218\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/media?parent=218"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/categories?post=218"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.appservgrid.com\/paw93\/index.php\/wp-json\/wp\/v2\/tags?post=218"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}