# Hosting an Angular app in Kubernetes

- URL: https://baeke.info/2019/03/15/hosting-an-angular-app-in-kubernetes/
- Author: Geert Baeke
- Published: March 15, 2019 (updated March 12, 2025)
- Topics: api, cloud, containers, dev, kubernetes, angular, environment, kubernetes, routing

We recently had to deploy an Angular application to Kubernetes in three different environments: development, acceptance and production. The application is not accessed via the browser directly. Instead, it’s accessed via a Microsoft Office [add-in](https://docs.microsoft.com/en-us/office/dev/add-ins/overview/office-add-ins).

The next sections will provide you with some tips to make this work. In practice, I do not recommend hosting static sites in Kubernetes. Instead, host such sites in a [storage account with a CDN](https://baeke.info/2019/01/11/static-site-hosting-on-azure-storage-with-a-custom-domain-and-tls/) or use [Azure FrontDoor](https://baeke.info/2019/01/12/azure-front-door-in-front-of-a-static-website/).

### Build and release pipelines

We keep our build and release pipelines as simple as possible. The build pipeline builds and pushes a Docker image and creates a Helm package:

![](https://baeke.info/_astro/2019-03-image.D6OhC8D1_2lG8ba.webp)

*Build pipeline*

The **Helm Package** task merely packages the Helm chart in the linked git repository in a .tgz file. The .tgz file is published as an artifact, to be picked up by the release pipeline.

The release pipeline simply uses the **helm upgrade** command via a Helm task provided by Azure DevOps:

![](https://baeke.info/_astro/2019-03-image-1.D_9FtOvn_Z2p8GVY.webp)

*Release pipeline*

**Before we continue**: these build and release steps actually just build an image to use as an initContainer in a Kubernetes pod. Why? Read on… 😉

## initContainer

Although we build the Angular app in the build pipeline, we actually don’t use the build output. We merely build the app provisionally to cancel the build and subsequent release when there is an error during the Angular build.

In the release pipeline, we again build the Angular app **after** we updated [environment.prod.ts](http://tattoocoder.com/angular-cli-using-the-environment-option/) to match the release environment. First read up on the use of environment.ts files to understand their use in an Angular app.

In the development environment for instance, we need to update the environment.prod.ts file with URLs that match the development environment URLs **before** we build:

```bash
export const environment = {
  production: true,
  apiUrl: '#{apiUrl}#',
  adUrl: '#{adUrl}#',
};
```

The actual update is done by a shell script with trusty old sed:

```
#!/bin/bash

cd /app/src/environments
sed -i "s|#{apiUrl}#|$apiUrl|g" environment.prod.ts
sed -i "s|#{adUrl}#|$adUrl|g" environment.prod.ts

mkdir /usr/share/nginx/html/addin -p

npm install typescript@">=2.4.2 <2.7"
npm run build -- --output-path=/app/dist/out --configuration production --aot

cp /app/dist/out/* /usr/share/nginx/html/addin -r
```

The shell script expects environment variables $apiUrl and $adUrl to be set. After environment.prod.ts is updated, we build the Angular app with the correct settings for apiUrl and adUrl to end up in the transpiled and minified output.

The actual build happens in a Kubernetes initContainer. We build the initContainer in the Azure DevOps build pipeline. We don’t build the final container because that is just default nginx hosting static content.

Let’s look at the template in the Helm chart (just the initContainers section):

```
initContainers:
  - name: officeaddin-build
    image: {{ .Values.images.officeaddin }}
    command: ['/bin/bash', '/app/src/deploy.sh']
    env:
     - name: apiUrl
       value: {{ .Values.env.apiUrl | quote }}
     - name: adUrl
       value: {{ .Values.env.adUrl | quote }}
    volumeMounts:
    - name: officeaddin-files
      mountPath: /usr/share/nginx/html
```

In the above YAML, we can identify the following:

-   **image**: set by the release pipeline via a Helm parameter; the image tag is retrieved from the build pipeline via $(Build.BuildId)
-   **command**: the deploy.sh Bash script as discussed above; it is copied to the image during the build phase via the Dockerfile
-   environment variables (**env**): inserted via a Helm parameter in the release pipeline; for instance **env.apiUrl=$(apiUrl)** where $(apiUrl) is an Azure DevOps variable
-   **volumeMounts**: in another section of the YAML file, an emptyDir volume called **officeaddin-files** is created; that volume is mounted on the initContainer as /usr/share/nginx/html; deploy.sh actually copies the Angular build output to that location so the files end up in the volume; later, we can map that volume to the nginx container that hosts the website

After the initContainer successfully builds and copies the output, the main nginx container can start. Here is the Helm YAML (with some stuff left out for brevity):

```
containers:
  - name: officeaddin
    image: nginx
    ports:
      - name: http
        containerPort: {{ .Values.service.port}}
    volumeMounts:
      - name: officeaddin-files
        mountPath: /usr/share/nginx/html
      - name: nginx-conf
        readOnly: true
        mountPath: /etc/nginx/conf.d
```

The officeaddin-files volume with the build output from the initContainer is mounted on /usr/share/nginx/html, which is where nginx expects your files by default.

## Nginx config for Angular

The default nginx config will not work. That is the reason you see an additional volume being mounted. The volume actually mounts a configMap on /etc/nginx/conf.d. Here is the configMap:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  default.conf: |
    server {
      server_name addin;

      root /usr/share/nginx/html ;

      location / {
        try_files $uri $uri/ /addin/index.html?$args;
      }
    }
```

The above configMap, combined with the volumeMount, results in a file /etc/nginx/conf.d/default.conf. The default nginx configuration in /etc/nginx/nginx.conf will inlude all files in /etc/nginx/conf.d. The nginx configuration in that file maps all requests to /addin/index.html, which is exactly what we want for an Angular app (or React etc…).

### Ingress Controller

The Angular app is published via a Kubernetes [Ingress Controller](https://kubernetes.io/docs/concepts/services-networking/ingress/). In this case, we use [Voyager](https://appscode.com/products/voyager/). We only need to add a rule to the Ingress definition that routes request to the appropriate NodePort service:

```
rules:
  - host: {{ .Values.ingress.url | quote }}
    http:
      paths:
      - path: /addin/
        backend:
          serviceName: officeaddin-service
          servicePort: {{ .Values.service.port }}
```

Besides the above change, nothing special needs to be done to publish the Angular app.
