Docker, Angular and The 12 Factor App: Config

Docker, Angular and The 12 Factor App: Config

Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not. The 12 Factor App: Config

The Angular framework, by default unfortunately does not work in this manner. It does provide a great way of including environment configurations… but it doesn’t quite conform to the standard of “The 12 Factor App”.

Angular supplies us with src/environments folder which by default includes 2 files environment.ts and environment.prod.ts. It is possible to create an environment file for each environment you wish to deploy to. There are 2 issues with this:

You need to run ng build with the relevant environment flag which then bakes in the config to the final rendered app. The code that you render for UAT can’t be built with Angular’s ng build --prod which means the code being tested on UAT and the code in production will be different (no AOT compiling etc)

A Simple Solution

Add a variable to both environment.ts and environment.prod.ts:

export const environment = {
  welcomeMessage: 'Hello World'
};

Reference the environment variable in your component

# app.component.ts
import {Component} from '@angular/core';
import {environment} from '../environments/environment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  welcomeMessage = environment.welcomeMessage;
}
# app.component.html
<div style="text-align:center">
  <h1>{{welcomeMessage}}!</h1>
</div>

Run ng build in your terminal to see the result. You should see a title of Hello World, taken from our env file.

Next we need to make sure we can inject our environment config after the build has taken place. Enter Docker and a simple token string replacement.

Open environment.prod.ts and do the following:

export const environment = {
  welcomeMessage: '{{{WELCOME_MESSAGE}}}',
  production: true
};

Create Dockerfile in the root directory of the project.

# Install node dependencies. Keep these in a seperate container named 'base' to speed up deployment.
FROM node:alpine AS dependencies
WORKDIR /usr/src/app
COPY package*.json ./

RUN npm install

# Build angular project
FROM dependencies AS build
COPY src src
COPY angular.json .
COPY tsconfig.json .
COPY tsconfig.app.json .

RUN npm run build

# Setup nginx and use entryPoint.sh to string replace config tokens.
FROM nginx:alpine
COPY --from=build usr/src/app/dist/ /usr/share/nginx/html
COPY ./build/nginx.conf /etc/nginx/
COPY ./build/default.conf /etc/nginx/conf.d/
COPY ./build/entryPoint.sh /entryPoint.sh

ENTRYPOINT ["/bin/sh", "/entryPoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

Docker is building here in 3 phases. 1st, install dependencies. 2nd, build the angular app. 3rd, serve with nginx.

The secret sauce to this happens when entryPoint.sh runs as th entrypoint of the docker file.

#!/bin/sh
set -xe
: "${WELCOME_MESSAGE?Need an api url}"

ls /usr/share/nginx/html/

sed -i "s~{{{WELCOME_MESSAGE}}}~$WELCOME_MESSAGE~g" /usr/share/nginx/html/main-es5.*.js
sed -i "s~{{{WELCOME_MESSAGE}}}~$WELCOME_MESSAGE~g" /usr/share/nginx/html/main-es2015.*.js

exec "$@"

The entryPoint.sh is basically doing a string replacement of the token we have created in our environment.ts with the string which will we provide docker when we run the project.

So lets give it a go:

  • docker build -t angular-12-factor-config .
  • docker run -e WELCOME_MESSAGE='Welcome to 12 Factor' --publish 8080:80 angular-12-factor-config:latest
  • navigate to http://localhost:8080/ and you should see our 'Hello World' message has changed to 'Welcome to 12 Factor'

This is obviously a very simple example, but you can expand this by simply adding more tokens to your environment.prod.ts and the entryPoint.sh. The most popular user case will be managing api url's between different hosting environments.

Full example

A full working example can be found on github