DEV Community

Cover image for Dynamic Api Url for your frontend project using Docker
Jacer Omri
Jacer Omri

Posted on

Dynamic Api Url for your frontend project using Docker

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:

// 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}`);
});
Enter fullscreen mode Exit fullscreen mode

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

# 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;"]
Enter fullscreen mode Exit fullscreen mode

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:

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

...
export const environment = {
  production: ${isProd},
  apiBaseUrl: 'API_BASE_URL',
  version: 'v${require('../package.json').version}'
};
`;
...
Enter fullscreen mode Exit fullscreen mode

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:

# 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"]
Enter fullscreen mode Exit fullscreen mode

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:

// 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;"
Enter fullscreen mode Exit fullscreen mode

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!

docker run -p 3000:3000 -e API_BASE_URL=http://myawesomebackend.com/api front
Enter fullscreen mode Exit fullscreen mode

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
Credit: Cover image from https://medium.com/developer-diary/quick-start-guide-for-docker-with-angular-on-windows-492263edeaf8

Top comments (4)

Collapse
 
appelsappel profile image
Appelsappel • Edited

Hey Jacer, nice article. I have a problem running the script:

$ docker run -p 3003:80 -e REACT_APP_API_URL=hondje 07afdda70bba
: not found_url.sh: 2: replace_api_url.sh:
find: missing argument to `-exec'

I copied your sh script and changed only the variable:

!/usr/bin/env sh

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

I'm not great with shell so could you help with this?

Collapse
 
appelsappel profile image
Appelsappel

hey, I got it to work when I replace the '\' with a '+'

Collapse
 
eternal21 profile image
Eternal21

Thanks for sharing a very clever pattern that just works. I used a slightly modified version where instead of rewriting all js output files, I only modify the config.json file that contains the base url of my api service.

Also, one issue I ran into by following your example, was docker not being able to find the replace_api_url.sh file. The fix was prefixing the name with a forward slash as following:
CMD ["sh", "/replace_api_base.sh"]

Collapse
 
maheshyaddanapudi profile image
Mahesh Yaddanapudi

Nice one. Solved my design problem.