Angular dynamic settings
Tags: angular, webdev, devops, docker
Published: 2021-08-27 11:47:08

(You can find the code shown here in https://github.com/livioribeiro/angular-dynamic-settings-example)

When developing an Angular application, usually your application settings lives in `src/environments/environment.ts` for development and `src/environments/environment.prod.ts` for production, and Angular takes care of swapping those during a production build.

But if we have another environment, a *staging* environment, then we have a problem, since we expect to approve the application running in staging and promote the same code to production, but with Angular's approach to configuration we need to run another build to configure our app to production.

To overcome this problem, I came up a very simple, yet very effective, strategy:

- Load a javascript file before the application starts that will define a settings object in `window.$environment`. This is essentially the same as `environment.ts`.
- In `environment.ts`, export the object defined in `window.$environment`.
- Tell Angular to add the configuration directory to the build output directory.

First we need to create a directory called `src/config` and put the javascript file `environment.js` there:

```javascript
// src/config/environment.js
window.$environment = {
  production: false,
  api: "dev.my-backend.io",
  // and any other configuration that would go in "environment.ts"
};
```

And then load the script on `index.html`:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MyApp</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- add the following line -->
  <script src="/config/environment.js"></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
```

Now, in `environment.ts`, export the configuration object:

```typescript
// src/environments/environment.ts

// this interface is just to making things more typed
interface Environment {
  production: boolean;
  api: string;
}

export const environment = (window as any).$environment as Environment;
```

And finally, change `angular.json` build options, adding `"src/config"` the the assets, and remove the `"fileReplacements"` completely. I also changed `"outputPath"` to just `"dist"`:

```json
...
"build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
    // "outputPath" is just "dist"
    "outputPath": "dist",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "tsconfig.app.json",
    "assets": [
      "src/favicon.ico",
      "src/assets",
      // add the following
      "src/config"
    ],
    "styles": [
      "src/styles.css"
    ],
    "scripts": []
  },
    "configurations": {
      "production": {
        "budgets": [
          {
            "type": "initial",
            "maximumWarning": "500kb",
            "maximumError": "1mb"
          },
          {
            "type": "anyComponentStyle",
            "maximumWarning": "2kb",
            "maximumError": "4kb"
          }
        ],
        // "fileReplacements" is removed
        "outputHashing": "all"
      },
...
```

We can safely remove `src/environments/environment.prod.ts`, we don't need it anymore.

Now we can inject a configuration script instead of doing another build.

This approach works great with docker and kubernetes, and we can test it right now!

First, we need a Dockerfile:

```Dockerfile
FROM node:latest as builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:latest
COPY --from=builder /app/dist/ /usr/share/nginx/html/
```

And a configuration to inject (I called it "environment.js"):

```javascript
// environment.js
window.$environment = {
  production: true,
  api: "prod.my-backend.io",
};
```

We now build the image and run the container with our new configuration:

```sh
docker build -t my-app .
docker run --name my-app \
  -it --rm \
  -p 8080:8080 \
  -v $PWD/environment.js:/usr/share/nginx/html/config/environment.js \
  my-app
```

With Kubernetes you can use a configmap to store the "environment.js" and mount it as a volume under "/usr/share/nginx/html/config".

And that's it! No more rebuilding angular for staging and production!