Accessing Runtime Environment Variables in a Dockerized Angular Application

This article presents a solution for dynamically setting environment variables at runtime in a Dockerized Angular application.

Accessing Runtime Environment Variables in a Dockerized Angular Application

When deploying Angular applications across different environments, it's crucial to manage configuration settings such as API endpoints or secret keys without hardcoding them into the source code. This article presents a solution for dynamically setting environment variables at runtime in a Dockerized Angular application.

The Problem

In Angular, environment configuration is typically handled through environment.ts and environment.prod.ts files in the src/environments folder. Here's a typical setup:

// src/environments/environment.ts
export const environment = {
   production: false,
   API_KEY: '1234_API_KEY_5678',
   ANOTHER_API_SECRET: '__ANOTHER_SECRET__'
};

These files are integral to the Angular build process, but if sensitive data like API keys are stored here, they become exposed once the repository is shared or public. We need a method to inject these variables at runtime, especially when deploying the same application across various environments with different configurations.

The Solution: Runtime Variables with Docker

Dockerizing the Angular Application

First, let's containerize the Angular application using Docker. Here's a sample

# Start with a Node.js 18 Alpine image.
FROM node:18-alpine as build

# Set the working directory inside the container to /app.
# This is where the app's files will be placed inside the container.
WORKDIR /app

# Install Angular CLI globally inside the container.
# This is necessary for building the Angular application.
RUN npm install -g @angular/cli@14.2.10

# Copy the package.json and package-lock.json files from the project directory
# to the /app directory in the container.
# These files define the project dependencies.
COPY ./package.json ./
COPY ./package-lock.json ./

# Install all dependencies defined in package.json.
# The --force flag ensures that the installation proceeds even if there are conflicts.
RUN npm install --force

# Copy all the project files into the /app directory in the container.
# This includes all source code, Angular configuration files, etc.
COPY . .

# Build the Angular application in production mode.
# The output will be stored in the /app/dist directory inside the container.
RUN ng build --configuration=production --output-path=dist

# Copy the server.js file (Node.js server script) to the container.
# This script is responsible for serving the Angular application at runtime.
COPY server.js ./

# Expose port 4200. This is the port that the Node.js server will listen on.
EXPOSE 4200

# The command to start the Node.js server using server.js.
# When the container starts, it will execute this command.
CMD ["node", "server.js"]

In the Dockerfile setup for your Angular application, we start by using `node:18-alpine` as the base image, selected for its small size and efficiency, ideal for containerized environments. The setup process begins with setting `/app` as the working directory in the Docker container, ensuring that all operations take place in this designated area.

The Dockerfile then proceeds to install Angular CLI globally within the container. This step is crucial for building the Angular application later in the process. Following this, `package.json` and `package-lock.json` files are copied into the container to define project dependencies, which are subsequently installed using `npm install --force`. This ensures all necessary dependencies are available in the container environment.

Next, all project files, including source code and Angular configurations, are copied into the container. With all the necessary files in place, the Angular application is built in production mode, with the build artifacts stored in the `dist` directory.

The `server.js` file is then added to the container. This script is key to serving the Angular application and handling runtime environment variables. To make the application accessible, port 4200 is exposed, which is where the Node.js server will listen for incoming requests. Finally, the Dockerfile specifies the command to start the Node.js server, effectively serving the Angular application within the container.

This Dockerfile thus encapsulates a complete setup for running a containerized Angular application, ensuring a consistent and isolated environment for deployment.

Creating a Custom Server with Express.js

The server.js file serves the static Angular files and provides an endpoint to serve a config.js file, which will contain our runtime environment variables.

const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 4200;
const angularAppPath = path.join(__dirname, 'dist');
app.use(express.static(angularAppPath));
app.get('/config.js', (req, res) => {
    res.type('.js');
    const configData = `
        window._env_ = {
            BACKEND_URL: "${process.env.BACKEND_API_URL || ''}",
        };
    `;
    res.send(configData);
});
app.get('*', (req, res) => {
    res.sendFile(path.join(angularAppPath, 'index.html'));
});
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

Modifying Angular to Use Runtime Variables

In your Angular index.html, include the config.js script:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title>Angular App</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>

<body class="mat-typography">
<app-root></app-root>
<script src="/config.js"></script>
</body>

</html>

Pay attention to the line that has

<script src="/config.js"></script>

Access these variables in your Angular service or component like so:

import { Injectable } from '@angular/core';

// Ensure that TypeScript is aware of the augmented Window interface
declare global {
    interface Window {
        _env_: {
            BACKEND_URL: string;
            DEMO_TOKEN: string;
        };
    }
}

@Injectable({
    providedIn: 'root',
})
export class DataService {
    private backendUrl: string;
    private demoToken: string;

    constructor() {
        // Accessing the environment variables from window._env_
        this.backendUrl = window._env_.BACKEND_URL;
        this.demoToken = window._env_.DEMO_TOKEN;
    }

    // Example method that uses the environment variables
    fetchData() {
        const url = `${this.backendUrl}/data`;
        // Use the url and token as needed
    }
}


Testing:

Build the Docker Image:

docker build -t angular-app .

Run the Docker Container:

docker run -p 4200:4200 -e BACKEND_API_URL=http://localhost:2000 angular-app
  • The -p 4200:4200 option maps port 4200 from the container to port 4200 on your host machine, allowing you to access the app via http://localhost:4200.
  • The -e flags set the environment variables BACKEND_API_URL in the container

Access the Application:

Open http://localhost:4200 in your web browser.

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow