Angular Multi-Environment Builds
This article is an in-depth guide to Angular build configuration. You’ll learn to serve, build, and distribute your project for different environments and deployment use cases.
What will we learn:
- Running Angular builds
- Serving dist static files in the browser
- Angular.json build configuration
- Angular environment files
Then, we’ll apply that knowledge by:
- Creating custom multi-environment configurations
- Separate dist per environment
Peek into Angular builds
For this guide, I created a new project: angular-multi-env app using Angular v18 and Node.js v20. The complete source code is in the repository linked at the bottom.
This is what the package.json file looks like:
{
"name": "angular-multi-env",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
}
Let’s do a quick build and see what happens.
We can do this in several ways:
> npm run build // or
> ng build // or
> ng b
Upon executing the build, Angular will generate a dist folder in the root directory.
> ng build
Initial chunk files | Names | Raw size | Estimated transfer size
main-O74TZ7OY.js | main | 206.72 kB | 55.67 kB
polyfills-FFHMD2TL.js | polyfills | 34.52 kB | 11.28 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes
| Initial total | 241.24 kB | 66.95 kB
Application bundle generation complete. [4.875 seconds]
Output location: .../angular-multi-env/dist/angular-multi-env
If we go to the specified location, you’ll find the browser directory containing all the distributable files.
These files represent the compiled (and compressed) code version that can run in the browser. However, we will need to do more than double-clicking on the index.html file. We need an HTTP server to serve these files.
Serving static files
We need to set up a backend server to serve Angular dist files in the browser. The backend API can be written in any language you desire, such as Express.js, Nginx, etc. The simplest way to set one is to install the
http-server npm package that runs a server in the CLI.
> npm i -g http-server
// once installed globally (-g), it can be used anywhere on the machine
Now, let’s use it to serve the files in the browser directory:
dist/angular-multi-env/browser>http-server
Starting up http-server, serving ./
http-server version: 14.1.1
http-server settings:
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none
Available on:
http://172.21.96.1:8081
http://192.168.0.14:8081
http://127.0.0.1:8081
Hit CTRL-C to stop the server
If you go to one of the specified IPs, the site should open as expected:
Understanding the Angular.json build configuration
By default, the Angular builds (and packages) files in one of two ways:
- Production
Slower build speed, optimized for performance and smaller bundle size. It uses AOT compilation, three-shaking, code minification, and disables source maps. - Development
Faster iteration by rebuilding the app in the browser on file changes. It includes source maps, hot module replacement, and debugging capabilities but lacks production optimizations.
Configuring builds
To find these builds, open up the angular.json file in the root directory and scroll down to the configurations section.
- projects /<your-project-name> / architect / build / configurations
{
"projects": {
"angular-multi-env": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-multi-env",
"index": "src/index.html",
"browser": "src/main.ts"
},
"configurations": {
"production": { // <-- production build
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": { // <-- development build
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
}
}
}
}
}
When we ran the build command (> ng build
), we created the application production build. How can we be certain?
First, if you open one of the files, you’ll notice that the code is so minified that it looks like snow.
The second reason is the default build configuration in the angular.json file. If we look up the build section
- projects /<your-project-name> / architect / build,
we’ll find the key that says: “defaultConfiguration”: “production”
:
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production" // <-- here it is
},
To run the development build, we can either change the “defaultConfiguration”
form “production”
to “development”
or run the build with the — configuration development
flag.
> ng build --configuration development
This will output more files, and the code inside will also differ.
Ng Serve configuration
Below the build configuration section, there is a serve section that describes how the app runs in the serve mode:
"projects": {
"angular-multi-env": {
"projectType": "application",
"root": "",
"prefix": "app",
"architect": {
...
"serve": { // <-- here it is
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-multi-env:build:production"
},
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
It also defaults to the development mode but can be changed to any other configuration option.
Distribution directory
The dist ( “distribution”) directory stores the application's compiled and optimized output after the build. The files in the dist folder are often versioned or hashed to facilitate cache busting (using outputHashing
), ensuring that users receive the latest version of your application after updates.
The location path of the dist folder is also set in the angular.json file under the outputPath
property.
- projects /<your-project-name> / architect / build / options
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/angular-multi-env", // <-- here it is
"index": "src/index.html",
"browser": "src/main.ts",
...
},
This can be modified to any location you desire.
In addition, you can have multiple dist folders, one for each environment in the configuration. We’ll learn more about that later.
Creating Environment files
While some frameworks use .env
files to store environment variables and API keys, Angular has a built-in file called environment.ts. Before version v15, the environment file was created upon project creation. In modern versions, they need to be generated manually:
> ng g environments
This command generates an environments directory in src directory, and two files inside:
- environment.ts
- environment.development.ts
It also updates the angular.json file, the development section with a new fileReplacements
property :
"development": {
...,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
We should only use the environment.ts file in the code, as it is the main env file for both the production and development environment.
The fileReplacements
property tells Angular that whenever we’re using the environment.ts
file in the given configuration mode (in this case, “development”), the contents of environment.ts
file will be overridden by those in the environment.development.ts
file.
Overriding environment files in action
Here is the default (production) environment file:
// environment.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.com/api',
greeting: 'Hello PROD!'
};
Also, the development environment file:
// environment.development.ts
export const environment = {
production: false,
apiBaseURL: 'http://localhost:3000/api',
greeting: 'Hello DEV!'
};
Two share the same keys but have different values. Now, import that environment file in the components and services, such as:
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = environment.greeting // <-- using the env file
}
Notice that we’re only using the default environment file.
If we run the DEV build, Angular will override the values from the main environment file with those from the environment development file.
Otherwise, in the production mode, the app will use the default environment.ts
file specified in the angular.json configuration.
Using the previously learned techniques, we’ll create a multi-env build configuration. Start by creating two additional env files (4 in total):
- environment.ts
- environment.production.ts
- environment.staging.ts
- environment.development.ts
DEV Configuration
We can make use of the environment file we created earlier:
// environment.development.ts
export const environment = {
production: false,
apiBaseURL: 'http://localhost:3000/api',
greeting: 'Hello DEV!'
};
Likewise, the angular.json configuration can also be updated, with optimization disabled, source maps enabled, and file replacements set:
"configurations": {
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
},
Configuration rules:
- Optimization: false
Indicates that this build will not be minified for performance purposes. This is something to use in production. - ExtractLicenses: false
Extract Licenses help with compliance by making license information easily accessible. This is not needed for DEV mode. - SourceMap: true
The source maps allow the source code to be displayed in the browser in development mode. This makes it easier to debug the application, but it should be avoided in production for security reasons. - FileReplacements
As discussed before, we use this setting to replace the defaultenvironment.ts
file with the one specified.
We’ll also keep the serve section in the angular.json file as is:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},
"defaultConfiguration": "development"
},
STAGING Configuration
Start with updating the (previously created) environment.staging.ts with some dummy values:
// environment.staging.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.staging.com/api',
greeting: 'Hello STAGING!'
};
Then we add the “staging” section in angular.json builder configurations:
"configurations": {
"staging": {
"aot": true,
"optimization": true,
"sourceMap": false,
"extractLicenses": true,
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "2MB",
"maximumError": "4MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.staging.ts"
}
]
}
},
We’ve created a hybrid of development and production configurations.
Configuration rules:
- AOT: true
Enables Ahead-of-Time (AOT) compilation, which pre-compiles Angular templates and components during the build process rather than at runtime. - Optimization: true
Indicates that this build will have tree-shaking and be minified for performance purposes. - SourceMap: false
The source code won’t be exposed in the browser. - ExtractLicenses: true
Extracts third-party licenses into a separate file. - Outputhashing: all
Appends a unique hash to filenames of output for JavaScript and CSS files, e.g., default:main.js
, with hashing:main-Y6OULDNJ.js
.
This ensures browser cache invalidation for updated files while keeping unchanged files cached. - Budgets
Sets the build size to warn or fail if certain size thresholds are exceeded. I increased the budget limits for staging from KB to MB to make build warnings less strict. - FileReplacements
Once again, thefileReplacements
property specifies that the default environment file will be replaced with a newly createdenvironment.staging.ts
file.
We’ll also add the staging configuration to the server builder section:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"staging": {
"buildTarget": "angular-multi-env:build:staging"
},
"development": {
...
}
},
"defaultConfiguration": "development"
},
PRODUCTION Configuration
The newly created environment.production.ts will have the same configuration as the default environment.ts. Two coexist because having the production config in the production env file is much more convenient.
// environment.production.ts
export const environment = {
production: true,
apiBaseURL: 'https://myapp.com/api',
greeting: 'Hello PROD!'
};
Now, onto updating the production configuration file:
"configurations": {
"production": {
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"fileReplacements": [ // <-- here it is
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
}
],
}
}
This config excludes most of the rules set in the staging configuration build because AOT, Optimization, ExtractLicenses, and Tree-Shaking are all enabled by default in the production build. The source maps are disabled in production by default.
As with previous cases, we’ll add the fileReplacements
property that overrides the contents of the default environment file with the production env file.
The serve configuration can stay as it is:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-multi-env:build:production"
},
"staging": {
"buildTarget": "angular-multi-env:build:staging"
},
"development": {
"buildTarget": "angular-multi-env:build:development"
}
},
Ng Serve per Environment
The ng serve
command is used to build and serve the application in the browser, rebuilding on file changes. It runs like this:
> ng serve
The default to serve is in the development mode, as specified in the angular.json file:
"projects": {
"angular-multi-env": {
...,
"architect": {
...
"serve": {
"configurations": {
"production": {...},
"development": {...}
},
"defaultConfiguration": "development" // <-- here it is
}
}
}
}
Since we separated the application into multiple environments, we can now serve content per environment as well:
// DEV (uses environment.development.ts)
> ng serve --configuration development
// STAGING (uses environment.staging.ts)
> ng serve --configuration staging
// PRODUCTION (uses environment.production.ts)
> ng serve --configuration production
We now have the power of hot reloading with ng serve
and use environment-specific variables.
Additional build scripts
Now that we are done with environment configurations, we’ll add new NPM scripts in the package.json file that will make it easier to build the application for each environment:
// package.json file
"scripts": {
"ng": "ng",
"start": "ng serve",
// new build scripts:
"build:development": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build:production": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
With this in place, we can easily generate a build for each environment:
// DEV
> npm run build:development
// STAGING
> npm run build:staging
// PRODUCTION
> npm run build:production
Using NPM Hooks with Builds
Furthermore, we can add pre- and post-scripts to perform actions before and after the build. For instance, we can create a script that starts an HTTP server that runs the static files from the dist folder and tell it to run after every production build.
"postbuild:production": "http-server dist/angular-multi-env/browser"
// package.json
"scripts": {
"ng": "ng",
"start": "ng serve",
"build:development": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build:production": "ng build --configuration production",
// automatically trigerred AFTER build:production
"postbuild:production": "http-server dist/angular-multi-env/browser",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
> npm run build:production
// PROD BUILD
> angular-multi-env@0.0.0 build:production
> ng build --configuration production
Initial chunk files | Names | Raw size | Estimated transfer size
main-Y6OULDNJ.js | main | 206.79 kB | 55.73 kB
polyfills-FFHMD2TL.js | polyfills | 34.52 kB | 11.28 kB
styles-5INURTSO.css | styles | 0 bytes | 0 bytes
| Initial total | 241.31 kB | 67.01 kB
Application bundle generation complete. [4.709 seconds]
Output location: angular-multi-env/dist/angular-multi-env
// POST PROD-BUILD
> angular-multi-env@0.0.0 postbuild:production
> http-server dist/angular-multi-env/browser
Starting up http-server, serving dist/angular-multi-env/browser
http-server version: 14.1.1
...
Available on:
http://172.21.96.1:8081
Finally, we can separate distributable folders per environment for dev, staging and production.
Start by setting the default build to dist.dev:
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist.dev/angular-multi-env", // <-- default
...
},
}
}
Then include the unique output path into each build environment build configuration:
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist.dev/angular-multi-env",
...
},
"configurations": {
"production": { // prod build
"outputPath": "dist.prod/angular-multi-env",
...
},
"staging": { // staging build
"outputPath": "dist.staging/angular-multi-env",
...
},
"development": { // dev build
"outputPath": "dist.dev/angular-multi-env",
...
}
},
"defaultConfiguration": "production"
},
Rerunning the same three build scripts:
// DEV
> npm run build:development
// STAGING
> npm run build:staging
// PRODUCTION
> npm run build:production
Creates an environment-specific dist folder:
Make sure also to add these paths to the git ignore file:
// .gitignore
# Compiled output
/dist
/dist.*
Full Code
Be sure to hit the follow button for more Angular content!