Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx
<h2 id="config-strategy">Designing a Configuration Strategy</h2>
<p>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.</p><figure style="margin:20px 0"><img src="https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vwcoj9exa4a6au5y7r4.png" alt="Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx" style="width:100%;height:auto;border-radius:8px" loading="lazy"><figcaption style="font-size:12px;color:#666;margin-top:5px">Source: dev.to</figcaption></figure>
<h3 id="creating-config-files">Creating Environment-Specific Configuration Files</h3>
<p>In your Angular project's <code>src/config</code> folder, create JSON files for each environment, for example <code>app-config.dev.json</code> and <code>app-config.prod.json</code>. Additionally, place a default <code>app-config.json</code> directly in the <code>src/</code> folder for local development. This approach ensures that the application can be tested locally with its typical configuration while keeping environment-specific values separate.</p>
<h3 id="including-config-files">Including Config Files in Build Assets</h3>
<p>To ensure these files are shipped with the built application, update the <code>angular.json</code> configuration file under the project's <code>architect.build.options.assets</code> array. Add the following entries:</p>
<pre><code>"assets": [
"src/favicon.ico",
"src/assets",
"src/app-config.json",
"src/config"
]</code></pre>
<p>This will copy the config folder and the default config file into the build output, making them accessible at runtime.</p>
<h2 id="angular-implementation">Implementing Dynamic Configuration Loading in Angular</h2>
<p>The Angular application must load the correct configuration before the app initializes, using the <code>HttpClient</code> to fetch the JSON file that will be set by the Docker entrypoint.</p>
<h3 id="config-service">Building a Config Service</h3>
<p>Create an injectable service that fetches the configuration file from the server root. The service uses Angular's <code>HttpClient</code> and returns a promise that resolves when the config is loaded.</p>
<pre><code>@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;
}
}</code></pre>
<p>Note: The path <code>./app-config.json</code> refers to the root of the web server, which in our Docker image will point to the Nginx served folder.</p>
<h3 id="app-initializer">Initializing the App with a Bootstrap Guard</h3>
<p>To prevent the application from starting before the configuration is available, use Angular's <code>APP_INITIALIZER</code> provider. This can be added in <code>app.config.ts</code> (for standalone applications) or in the <code>AppModule</code> for module‐based setups.</p>
<pre><code>export function initApp(configService: ConfigService) {
return () => configService.loadConfig();
}
// In providers array:
{
provide: APP_INITIALIZER,
useFactory: initApp,
deps: [ConfigService],
multi: true
}</code></pre>
<p>With this setup, the Angular app waits for <code>ConfigService.loadConfig()</code> to complete before rendering any components, ensuring that all environment-specific values are available.</p>
<h2 id="docker-containerization">Containerizing with Docker: The Entrypoint Approach</h2>
<p>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 <code>ENVIRONMENT</code> environment variable.</p>
<h3 id="entrypoint-script">Crafting the Entrypoint Script</h3>
<p>Create a file named <code>entrypoint.sh</code> in the project root. This shell script runs when the container starts, checks the <code>ENVIRONMENT</code> variable, and overwrites the default <code>app-config.json</code> with the appropriate environment-specific file.</p><figure style="margin:20px 0"><img src="https://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F347695%2F0883cd79-cada-4653-b32b-7db298946057.jpeg" alt="Unified Angular Deployment: One Build, Environment-Specific Configs via Docker and Nginx" style="width:100%;height:auto;border-radius:8px" loading="lazy"><figcaption style="font-size:12px;color:#666;margin-top:5px">Source: dev.to</figcaption></figure>
<pre><code>#!/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;'</code></pre>
<p>Note: The paths assume that the Nginx root is <code>/usr/share/nginx/html</code>, which is the default in the official Nginx image. The script copies the correct config file over the default <code>app-config.json</code> before starting Nginx.</p>
<h3 id="dockerfile">Building the Multi-Stage Dockerfile</h3>
<p>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.</p>
<pre><code># 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"]</code></pre>
<p>Replace <code>your-app-name</code> with the actual output directory name from your Angular build (usually found in <code>dist/</code>). The entrypoint is set as the container startup command.</p>
<h2 id="running-container">Running the Container Across Environments</h2>
<p>With the Docker image built, you can now deploy the same image to any environment simply by passing the appropriate <code>ENVIRONMENT</code> variable.</p>
<h3 id="building-image">Building the Docker Image</h3>
<p>Run the following command in the project root to build the image once:</p>
<pre><code>docker build -t my-angular-app .</code></pre>
<h3 id="running-with-env">Running with Environment Variables</h3>
<p>Start the container for different environments by setting the <code>ENVIRONMENT</code> variable:</p>
<ul>
<li><strong>Production</strong>: <code>docker run -e ENVIRONMENT=Prod -p 8080:80 my-angular-app</code></li>
<li><strong>Development</strong>: <code>docker run -e ENVIRONMENT=Dev -p 8080:80 my-angular-app</code></li>
</ul>
<p>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.</p>
<h2 id="conclusion">Conclusion</h2>
<p>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.</p>