Dynamic Api Url for your frontend project using Docker
Tags: angular, docker, javascript, devops
Published: 2020-03-01 00:07:43

If you're doing frontend web developing you know you have to keep some aspect of your app configurable. The most obvious case of this configuration is the API base url. You should define it in a way you could pass it as an environment variable in the build process.

When you're building with docker, it means this becomes a build argument, as opposed to container argument.

This is not ideal. Because then you should have a separate and different docker image for each environment (say canary, staging and production).

I'll be sharing with you a solution i came up with to solve this in all of our frontend projects. I'll be using an Angular project to illustrate.

# Before
The first solution i had is to include a script to create the specific environment.something.ts file before the build process.

Here is how it looks like:

```typescript
// FILE: front/scripts/set-env.ts

import { writeFile } from 'fs';
import { argv } from 'yargs';

// This is good for local dev environments, when it's better to
// store a projects environment variables in a .gitignore'd file
require('dotenv').config();

// Would be passed to script like this:
// `ts-node set-env.ts --environment=dev`
// we get it from yargs's argv object
const environment = argv.environment;
const isProd = environment === 'prod';

const targetPath = environment === 'dev'
  ? `./src/environments/environment.ts`
  : `./src/environments/environment.${environment}.ts`;
const envConfigFile = `
export const environment = {
  production: ${isProd},
  apiBaseUrl: '${process.env.API_BASE_URL}',
  version: 'v${require('../package.json').version}'
};
`;

writeFile(targetPath, envConfigFile, function (err) {
  if (err) {
    console.log(err);
  }

  console.log(`Output generated at ${targetPath}`);
});
```

And i run the script in the docker build process as the following:

```Dockerfile
# FILE: Dockerfile

### STAGE 1: Build ###

# We label our stage as 'builder'
FROM node:10-alpine as builder
ARG NODE_ENV
ARG API_BASE_URL
ENV NODE_ENV "$NODE_ENV"
ENV API_BASE_URL "$API_BASE_URL"

COPY package.json package-lock.json ./
RUN npm set progress=false && npm config set depth 0 && npm cache clean --force
RUN npm install -g ts-node yargs dotenv typescript@2.4.2

## Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN npm i && mkdir /ng-app && cp -R ./node_modules ./ng-app
WORKDIR /ng-app
COPY . .
## Build the angular app in production mode and store the artifacts in dist folder
RUN ts-node ./scripts/set-env.ts --environment=prod #actually this is defined as a script in package.json, let's add it here so things would make sense.
RUN npm run build


### STAGE 2: Setup ###

FROM nginx:1.13.3-alpine

## Copy our default nginx config
COPY nginx/default.conf /etc/nginx/conf.d/

## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=builder /ng-app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
```

In the end, we end up with a very small docker file, containing nginx and only the build artifacts. No node_modules, no nodejs, nothing. This is good until you want to use the same image for multiple environments.
The first idea to come to mind is to ditch this 2 step build process and just ship the actual angular code and serve it on the fly using any runtime configuration. Right?

The only problem is, the docker image would get 200 times bigger, and this is a big no no.

# Solution!
Let's use some Linux skills and figure out how we could change the API base url in the compiled Javascript files!

First, that old script becomes like this:

```typescript
// FILE: front/scripts/set-env.ts

...
export const environment = {
  production: ${isProd},
  apiBaseUrl: 'API_BASE_URL',
  version: 'v${require('../package.json').version}'
};
`;
...
```
Basically, we are passing this string `'API_BASE_URL'` in the configuration.

Next, we need to find and replace that string in every compiled Javascript file in the resulting image. Let's make a small adjustment in the Dockerfile:

```Dockerfile
# FILE: Dockerfile

...
COPY --from=builder /ng-app/dist /usr/share/nginx/html
COPY --from=builder /ng-app/scripts/replace_api_url.sh /

CMD ["sh", "replace_api_url.sh"]
```

We did 2 things here:
* added a new script called `replace_api_url.sh` which we'll see in a minute
* and replaced the command line so it would no longer launch nginx directly, instead, we want to execute our newly added script.

Finally, this is what the new script looks like:

```typescript
// FILE: front/scripts/replace_api_url.sh

#!/usr/bin/env sh

find '/usr/share/nginx/html' -name '*.js' -exec sed -i -e 's,API_BASE_URL,'"$API_BASE_URL"',g' {} \;
nginx -g "daemon off;"
```

So we're launching nginx at the end, but we're expecting an environment variable *at runtime* to replace every occurrence of `API_BASE_URL` in all js files.

Now you only need to pass any API url when running a new docker contanaier!
```bash
docker run -p 3000:3000 -e API_BASE_URL=http://myawesomebackend.com/api front
```

Thats's it!

Thank you for reading, this is my first post here and would like to have your feedback so i can continue writing better and better stuff here.

![THE END](https://www.mememaker.net/api/bucket?path=static/img/memes/full/2015/Mar/3/2/you-made-it-to-the-end.jpg)
_Credit: Cover image from https://medium.com/developer-diary/quick-start-guide-for-docker-with-angular-on-windows-492263edeaf8_