Modern Angular applications often need different configuration values depending on where they are deployed. API endpoints, feature flags, authentication settings, logging options, and third-party service identifiers may differ between development, testing, staging, and production environments.
Angular provides built-in support for environment-specific build configurations, but there is an important distinction developers need to understand: build-time configuration is not the same as runtime environment variables.
This article explains how to use environment variables in Angular components, how Angular environment files work, when to use runtime configuration, and which approach is best for applications deployed with Docker, Kubernetes, or CI/CD pipelines.
Understanding Environment Configuration in Angular
Unlike a traditional backend application, an Angular application executes primarily inside the user’s browser. This means it cannot directly access operating-system environment variables such as:
API_URL=https://api.example.com
CLIENT_ID=my-client
after the application has already been built and delivered to the browser.
Instead, configuration usually follows one of two models:
- Build-time configuration using Angular environment files.
- Runtime configuration loaded when the application starts.
Choosing between these approaches is important when designing an Angular application.
Using Angular Environment Files
Angular supports environment-specific configuration through its build system. Different configuration files can be substituted depending on the selected build configuration.
A typical project might contain:
src/
└── environments/
├── environment.ts
├── environment.development.ts
└── environment.staging.ts
If the environment files do not exist, Angular CLI can generate and configure them:
ng generate environments
Angular officially provides this command specifically for creating and configuring environment files.
A production configuration could look like this:
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
enableLogging: false
};
The development version could contain:
export const environment = {
production: false,
apiUrl: 'http://localhost:8080',
enableLogging: true
};
Importing Environment Variables into an Angular Component
Once the environment configuration exists, it can be imported directly into a component:
import { Component } from '@angular/core';
import { environment } from '../environments/environment';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
readonly apiUrl = environment.apiUrl;
readonly production = environment.production;
}
The component can then use these values normally:
loadData(): void {
console.log(`Connecting to ${environment.apiUrl}`);
}
Angular recommends importing the base environment file rather than importing a specific environment such as environment.development.ts. The build configuration determines which implementation is used.
This keeps application code independent from the deployment target.
How Angular Selects the Correct Environment
Angular’s build configuration can use fileReplacements to substitute files for specific targets.
A simplified angular.json configuration might contain:
{
"configurations": {
"development": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
}
}
A development build can then be created using:
ng build --configuration development
while another configuration could be selected with:
ng build --configuration staging
Angular CLI supports named configurations and even allows multiple configurations to be applied in sequence.
This architecture makes environment files particularly useful for values known when the application is built.
Build-Time Configuration vs Runtime Configuration
One of the most important Angular configuration concepts is understanding when environment values become available.
With:
import { environment } from '../environments/environment';
the configuration becomes part of the compiled JavaScript bundle.
Consider:
export const environment = {
apiUrl: 'https://api.example.com'
};
After:
ng build
the API URL is effectively embedded into the generated application.
Changing an operating-system environment variable afterward will not automatically modify the Angular application.
This distinction becomes especially important with containerized deployments.
Angular Environment Files Are Not for Secrets
Developers should never place sensitive credentials in Angular environment files.
For example, this is unsafe:
export const environment = {
databasePassword: 'secret-password',
privateApiKey: 'private-key'
};
Angular’s documentation explicitly warns that files under src/environments/ are bundled into the client-side application and therefore visible to users. Secrets should instead remain on the server side or in an appropriate secrets-management system.
Values normally suitable for frontend configuration include:
API base URLs
public OAuth client IDs
feature flags
application version
analytics identifiers
logging configuration
public service endpoints
Values that should remain server-side include:
database passwords
private API keys
client secrets
private certificates
service-account credentials
signing keys
A useful rule is:
If the browser needs the value, assume the user can see it.
Runtime Configuration in Angular
Build-time environments work well when a separate application build is created for each environment.
However, modern deployment architectures often aim to:
Build once, deploy everywhere.
For example, the same Docker image might be promoted through:
Development
↓
Testing
↓
Staging
↓
Production
Rebuilding the Angular application for every environment defeats some of the benefits of immutable container images.
Runtime configuration provides an alternative.
Loading Runtime Configuration from JSON
A common solution is to create a configuration file such as:
assets/config.json
Example:
{
"apiUrl": "https://api.example.com",
"enableLogging": false,
"featureXEnabled": true
}
Angular can load this configuration when the application starts.
A service might be defined as:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface AppConfig {
apiUrl: string;
enableLogging: boolean;
featureXEnabled: boolean;
}
@Injectable({
providedIn: 'root'
})
export class ConfigService {
private readonly http = inject(HttpClient);
private config!: AppConfig;
load(): Promise<void> {
return new Promise((resolve, reject) => {
this.http.get<AppConfig>('/assets/config.json')
.subscribe({
next: config => {
this.config = config;
resolve();
},
error: reject
});
});
}
get apiUrl(): string {
return this.config.apiUrl;
}
get featureXEnabled(): boolean {
return this.config.featureXEnabled;
}
}
Components can then consume the configuration through dependency injection:
import { Component, inject } from '@angular/core';
import { ConfigService } from './config.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html'
})
export class DashboardComponent {
private readonly config = inject(ConfigService);
readonly apiUrl = this.config.apiUrl;
}
Angular’s dependency injection system supports supplying both services and configuration values to components, making DI a natural abstraction for centralized application configuration.
Using inject() Instead of Constructor Injection
Traditionally, Angular services were commonly injected through constructors:
constructor(private configService: ConfigService) {}
Modern Angular also supports:
private readonly configService = inject(ConfigService);
Angular’s current style guidance prefers inject() over constructor parameter injection because it can improve readability and type inference, particularly when a class has multiple dependencies.
For example:
export class ProductComponent {
private readonly config = inject(ConfigService);
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
}
There is one important restriction: inject() must execute inside an Angular injection context, such as during component or service construction or within a field initializer. Calling it later from an arbitrary method or lifecycle hook is not valid.
Using an InjectionToken for Application Configuration
For larger applications, another clean approach is to expose configuration through Angular’s dependency injection system.
First define an interface:
export interface AppConfig {
apiUrl: string;
production: boolean;
}
Then create an injection token:
import { InjectionToken } from '@angular/core';
export const APP_CONFIG =
new InjectionToken<AppConfig>('APP_CONFIG');
The configuration can then be provided during application bootstrap:
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
production: true
}
}
]
A component can consume it with:
import { Component, inject } from '@angular/core';
import { APP_CONFIG } from './app-config';
@Component({
selector: 'app-example',
template: `...`
})
export class ExampleComponent {
readonly config = inject(APP_CONFIG);
}
InjectionToken is particularly useful for configuration because Angular DI can inject arbitrary values and objects, not only class instances.
This also improves testability because tests can easily provide a different configuration.
Runtime Configuration with Docker
Consider an Angular application packaged as a Docker image.
Instead of building separate images:
frontend:development
frontend:staging
frontend:production
an organization may prefer:
frontend:1.5.0
and deploy exactly that image everywhere.
At container startup, an environment-specific configuration file can be generated.
For example:
#!/bin/sh
cat <<EOF > /usr/share/nginx/html/assets/config.json
{
"apiUrl": "${API_URL}",
"enableLogging": ${ENABLE_LOGGING}
}
EOF
exec nginx -g "daemon off;"
The container could then receive:
API_URL=https://api.production.example.com
ENABLE_LOGGING=false
while staging uses:
API_URL=https://api.staging.example.com
ENABLE_LOGGING=true
The Angular application remains identical. Only its runtime configuration changes.
Runtime Configuration with Kubernetes
The same architecture works particularly well with Kubernetes.
Configuration can originate from:
ConfigMap
↓
Environment variables
↓
Container startup script
↓
config.json
↓
Angular ConfigService
↓
Angular components
For example:
env:
- name: API_URL
valueFrom:
configMapKeyRef:
name: frontend-config
key: api-url
The container startup process converts that environment value into configuration that the browser can retrieve.
This provides a useful separation between:
Application artifact
and:
Deployment configuration
and allows the same frontend image to move between environments without recompilation.
Combining Build-Time and Runtime Configuration
Many production Angular applications benefit from combining both techniques.
A practical architecture is:
Angular environment.ts
│
├── Build-related constants
├── compile-time feature selection
└── application defaults
Runtime config.json
│
├── API endpoints
├── environment-specific URLs
├── public integration IDs
└── deployment-specific settings
For example:
// environment.ts
export const environment = {
production: true,
applicationName: 'My Application',
version: '2.1.0'
};
while:
{
"apiUrl": "https://api.example.com",
"authenticationUrl": "https://auth.example.com"
}
contains deployment-specific values.
This gives developers the simplicity of Angular environments while retaining the flexibility required by modern deployment pipelines.
Common Mistakes to Avoid
Importing a Specific Environment File
Avoid:
import { environment }
from '../environments/environment.production';
Prefer:
import { environment }
from '../environments/environment';
Angular’s build configuration should determine the appropriate replacement.
Storing Secrets in Angular
Never rely on frontend code to hide:
passwords
private keys
API secrets
database credentials
Anything delivered to the browser must be considered accessible to the user.
Expecting process.env to Work Like Node.js
Angular browser applications are not Node.js backend processes.
Code such as:
const apiUrl = process.env.API_URL;
does not automatically provide access to environment variables from the server hosting the built Angular application.
The value must either be incorporated during the build process or exposed through a runtime configuration mechanism.
Scattering Configuration Throughout Components
Avoid hard-coded URLs such as:
this.http.get(
'https://production.example.com/api/products'
);
Instead, centralize configuration:
this.http.get(
`${this.config.apiUrl}/products`
);
This reduces duplication and makes testing and deployment substantially easier.
Recommended Architecture
For a conventional Angular application with separate builds for each environment:
environment.ts
environment.development.ts
environment.staging.ts
is usually sufficient.
For Angular applications deployed through modern CI/CD infrastructure:
Angular Application
│
ConfigService
│
Runtime Config
│
┌─────────────┴─────────────┐
│ │
Docker Kubernetes
│ │
ENV variables ConfigMap
is generally more flexible.
A mature application can therefore follow this principle:
Build-time environment files define application defaults; runtime configuration defines deployment-specific values; sensitive information remains on the backend.
Conclusion
Environment configuration may initially appear to be a minor Angular implementation detail, but it becomes an important architectural concern as an application moves from local development into automated CI/CD and containerized environments.
Angular’s environment files provide an effective mechanism for compile-time configuration, with Angular CLI supporting named configurations and file replacement.
For deployments where the same artifact should run in multiple environments, runtime configuration through JSON, dependency injection, or another external configuration source provides greater flexibility.
The most important principles are:
- Keep environment-dependent values centralized.
- Import the generic
environmentconfiguration rather than environment-specific files. - Understand the difference between build-time and runtime variables.
- Never store secrets in frontend environment files.
- Use Angular dependency injection to make configuration reusable and testable.
- Consider runtime configuration when deploying the same Angular artifact through Docker or Kubernetes.
Following these practices creates Angular applications that are easier to maintain, test, deploy, and scale across development, staging, and production environments.


