Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx

From Xtcworld, the free encyclopedia of technology

Designing a Configuration Strategy

Standard Angular builds embed environment variables directly into the code during compilation. This forces separate builds for each environment—Dev, Staging, and Production—which is inefficient and error-prone. A smarter approach is to compile a single Docker image and inject the correct configuration at runtime. This article details a robust method using environment-specific JSON configuration files and a Docker entrypoint script.

Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx
Source: dev.to

Creating Environment-Specific Configuration Files

In your Angular project's src/config folder, create JSON files for each environment, for example app-config.dev.json and app-config.prod.json. Additionally, place a default app-config.json directly in the src/ folder for local development. This approach ensures that the application can be tested locally with its typical configuration while keeping environment-specific values separate.

Including Config Files in Build Assets

To ensure these files are shipped with the built application, update the angular.json configuration file under the project's architect.build.options.assets array. Add the following entries:

"assets": [
  "src/favicon.ico",
  "src/assets",
  "src/app-config.json",
  "src/config"
]

This will copy the config folder and the default config file into the build output, making them accessible at runtime.

Implementing Dynamic Configuration Loading in Angular

The Angular application must load the correct configuration before the app initializes, using the HttpClient to fetch the JSON file that will be set by the Docker entrypoint.

Building a Config Service

Create an injectable service that fetches the configuration file from the server root. The service uses Angular's HttpClient and returns a promise that resolves when the config is loaded.

@Injectable({ providedIn: 'root' })
export class ConfigService {
  private config: any;

  constructor(private http: HttpClient) {}

  loadConfig() {
    return firstValueFrom(this.http.get('./app-config.json'))
      .then(data => this.config = data);
  }

  get settings() {
    return this.config;
  }
}

Note: The path ./app-config.json refers to the root of the web server, which in our Docker image will point to the Nginx served folder.

Initializing the App with a Bootstrap Guard

To prevent the application from starting before the configuration is available, use Angular's APP_INITIALIZER provider. This can be added in app.config.ts (for standalone applications) or in the AppModule for module‐based setups.

export function initApp(configService: ConfigService) {
  return () => configService.loadConfig();
}

// In providers array:
{
  provide: APP_INITIALIZER,
  useFactory: initApp,
  deps: [ConfigService],
  multi: true
}

With this setup, the Angular app waits for ConfigService.loadConfig() to complete before rendering any components, ensuring that all environment-specific values are available.

Containerizing with Docker: The Entrypoint Approach

Now we package the built Angular application into a Docker image using a multi‐stage build. The key is to use a custom entrypoint script that swaps the configuration files based on the ENVIRONMENT environment variable.

Crafting the Entrypoint Script

Create a file named entrypoint.sh in the project root. This shell script runs when the container starts, checks the ENVIRONMENT variable, and overwrites the default app-config.json with the appropriate environment-specific file.

Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx
Source: dev.to
#!/bin/bash
if [[ $ENVIRONMENT == "Prod" ]]; then
  cp /usr/share/nginx/html/config/app-config.prod.json /usr/share/nginx/html/app-config.json
else
  cp /usr/share/nginx/html/config/app-config.dev.json /usr/share/nginx/html/app-config.json
fi

nginx -g 'daemon off;'

Note: The paths assume that the Nginx root is /usr/share/nginx/html, which is the default in the official Nginx image. The script copies the correct config file over the default app-config.json before starting Nginx.

Building the Multi-Stage Dockerfile

Use two stages: one for building the Angular application with Node.js, and a second to serve it with Nginx. The final image is small and contains only the built files and the entrypoint script.

# Build Stage
FROM node:lts AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Run Stage
FROM nginx:latest
COPY --from=build /app/dist/your-app-name/browser /usr/share/nginx/html
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

Replace your-app-name with the actual output directory name from your Angular build (usually found in dist/). The entrypoint is set as the container startup command.

Running the Container Across Environments

With the Docker image built, you can now deploy the same image to any environment simply by passing the appropriate ENVIRONMENT variable.

Building the Docker Image

Run the following command in the project root to build the image once:

docker build -t my-angular-app .

Running with Environment Variables

Start the container for different environments by setting the ENVIRONMENT variable:

  • Production: docker run -e ENVIRONMENT=Prod -p 8080:80 my-angular-app
  • Development: docker run -e ENVIRONMENT=Dev -p 8080:80 my-angular-app

The same image works everywhere—no need to rebuild for each environment. The entrypoint script automatically picks the right configuration based on the environment variable.

Conclusion

By decoupling configuration from the build process, you can streamline Angular deployments, reduce build times, and eliminate environment-specific errors. The combination of runtime configuration loading in Angular and a Docker entrypoint script allows you to build once and deploy anywhere. This approach is production-ready and can easily be extended with additional environment files or more complex logic.