I am trying to containerize a frontend web application and I am having troubles to figure out how to pass environment variables. The application is a Angular application, so it is 100% client-side.
In a typical backend service, passing environment variables is easy, as everything is running on the same host, so the environment variables can be easily picked by the backend service. However, in a frontend application, this is different: the application is running in the browser of the client.
I want to configure my application via environment variables, as this makes deployment much easier. All configuration can be done in docker-compose.yml
and there is no need to maintain several images, one for every possible environment. There is just one single immutable image. This follows the 12-factor application philosophy, as can be found on https://12factor.net/config.
I am building my application image as following:
FROM node:alpine as builder
COPY package.json ./
RUN npm i && mkdir /app && cp -R ./node_modules ./app
WORKDIR /app
COPY . .
RUN $(npm bin)/ng build
FROM nginx:alpine
COPY nginx/default.conf /etc/nginx/conf.d/
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
In app/config.ts
, I have:
export const config = {
REST_API_URL: 'http://default-url-to-my-backend-rest-api'
};
Ideally, I want to do something like this in my docker-compose.yml
:
backend:
image: ...
frontend:
image: my-frontend-app
environment:
- REST_API_URL=http://backend:8080/api
So I believe I should alter this app/config.ts
to replace REST_API_URL
with the environment variable. As I prefer an immutable Docker image (so I do not want to do this replace during the build), I am quite puzzled how to progress here. I believe I should support to alter the app/config.ts
at runtime before the nginx proxy is started. However, the fact that this file is minified and webpack-bundled, makes this more diffucult.
Any ideas how to tackle this?
My solution: at run time use docker volumes to mount a specific js config file as the env.js.
I have a docker compose file for dev and prod.
I have dev.env.js and prod.env.js.
My html file references env.js.
In docker-compose.yml I volume mount either env file as env.js.
E.g. my dev compose:
And my prod compose:
I was struggling with the same problem but also needed to pass the configuration values from docker-compose level down to Angular, which I didn't find that straightforward.
Basically, I took a similar approach and came with the following solution:
docker-compose.yml
toDockerfile
using the compose ARGS. So indocker-compose.yml
I have:magicsword.core.web: build: args: - AUTH_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55888/ - GAME_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55889/ - GUI_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55890/ # =self
Dockerfile
as variables:ARG AUTH_SERVER_URL ARG GAME_SERVER_URL ARG GUI_SERVER_URL
RUN apt-get update && apt-get install -y gettext RUN envsubst < ./src/environments/environment.ts > ./src/environments/environment.ts.tmp && mv ./src/environments/environment.ts.tmp ./src/environments/environment.ts
The
environment.ts
before the substitution, for reference:export const environment = { production: true, GAME_SERVER_URL: "$GAME_SERVER_URL", GUI_SERVER_URL: "$GUI_SERVER_URL", AUTH_SERVER_URL: "$AUTH_SERVER_URL" };
Voila. Hope this helps someone :)
Put your environment variables in the
index.html
!!Trust me, I know where you are coming from! Baking environment-specific variables into the build phase of my Angular app goes against everything I have learned about portability and separation of concerns.
But wait! Take a close look at a common Angular
index.html
:This is all configuration!!!
It is just like the docker-compose.yml that you are using to maintain your Docker apps:
runtime
is like your base image that you rarely change.polyfills
are those things you need that didn't come included in the base image that you need.main
is your actual app that pretty much changes every release.You can do the same thing with your frontend app that you do with your Docker app!
How??
Just point the stinking
/src/environments/environment.prod.ts
at thewindow
object.and add a script to your index.html with the environment variable WHERE THEY BELONG!:
I feel so strongly about this approach I created a website dedicated to it: https://immutablewebapps.org. I think you will find there are a lot of other benefits!
~~~
Now, I have done this successfully using two AWS S3 Buckets: one for the versioned static assets and one for just the
index.html
(it makes routing super simple: serveindex.html
for every path). I haven't done it running containers like you are proposing. If I were to use containers, I would want to make a clean separation between the building and publishing new assets, and releasing of a newindex.html
. Maybe I would renderindex.html
on-the-fly from a template with the container's environment variables.If you choose this approach, I'd love to know how it turns out!
I had a similar problem for a static HTML file and here is what I wanted to solve:
I tried other answers but it seems like they didn't fit the above. So this is what I ended up with using
envsubst
Dockerfile
replaceEnvVars.sh
index.tmpl.html
docker-compose.yml
The way that I resolved this is as follows:
1.Set the value in the enviroment.prod.ts with a unique and identificable String:
2.Create a entryPoint.sh, this entryPoint will be executed every time that you done a docker run of the container.
As you can see, this entrypoint get the 'REST_API_URL_REPLACE' argument and replace it (in this case) in the main*bundle.js file for the value of the var.
3.Add the entrypoint.sh in the dockerfile before the CMD (it need execution permissions):
4.Lauch the image with the env or use docker-compose (the slash must be escaped):
Probably exists a better solution that not need to use a regular expresion in the minified file, but this works fine.