Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

Angular ng build Configuration: angular.json, Environments, and Deploy Output

Sean

Platform Writer

Sep 22, 2026
8 min read

The ng build configuration is a named block under projects..architect.build.configurations in angular.json, selected with —configuration=production or —configuration=staging, that overrides settings like fileReplacements, budgets, and output options for that build. Angular CLI v12 and later run production by default when no configuration is given. fileReplacements swap environment.ts for environment.prod.ts at build time, so the API URL and feature flags are baked into the bundle rather than read at runtime. The output lands in outputPath, normally dist/, and deploying that folder to a static host requires two things Angular itself does not configure: an SPA fallback rule so client-side routes resolve, and a baseHref set correctly if the app is served from a subpath.

Angular ng build Configuration: angular.json, Environments, and Deploy Output

ng build with no flags and ng build --configuration=production are the same command since Angular 12; the confusion in the older tutorials and Stack Overflow answers is a version gap, not a real distinction anymore. What still trips people up is what a configuration actually changes, and what happens after the build finishes, because Angular’s job stops at a folder of static files and the deploy is a separate decision.

Table of contents

What a configuration in angular.json controls

Each configuration is a named override under the build target. The base build options apply first, and the named configuration’s options merge on top, so a configuration only needs to state what is different from the default.

{
  "projects": {
    "app": {
      "architect": {
        "build": {
          "options": { "outputPath": "dist/app" },
          "configurations": {
            "production": {
              "budgets": [{ "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }],
              "outputHashing": "all",
              "fileReplacements": [
                { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" }
              ]
            },
            "staging": {
              "fileReplacements": [
                { "replace": "src/environments/environment.ts", "with": "src/environments/environment.staging.ts" }
              ]
            }
          }
        }
      }
    }
  }
}

Configurations can also be combined, for example --configuration=staging,cn if a locale configuration exists alongside staging. The CLI merges both in the order given, left to right, so a later configuration’s options win when the two overlap on the same key.

A configuration that is missing a key simply inherits it from the base options block rather than failing. This is why the staging configuration above only needs to list fileReplacements: everything else, outputPath, the absence of budgets, falls through from the shared options unless staging explicitly overrides it too.

Running the build with —configuration

# Production (default since Angular 12, explicit here for clarity)
ng build --configuration=production

# Staging
ng build --configuration=staging

# Older CLI versions used --prod, now an alias that maps to --configuration=production

The default in angular.json is set with "defaultConfiguration": "production" on the build target, which is why a bare ng build in a modern CLI is a production build, not a development one. Check that key if a bare ng build is producing an unminified bundle.

A configuration name that does not exist in angular.json fails immediately with a clear error naming the missing configuration, rather than silently falling back to the base options, which makes a typo in a CI script easy to spot the first time the pipeline runs it. That is a deliberate difference from the merge behaviour above: an unknown configuration is a hard error, a known one with missing keys is not.

fileReplacements: build-time environment swapping

fileReplacements is a compile-time substitution. The file named in replace is swapped for the file named in with before TypeScript compiles, so environment.ts in application code always imports the same module path, but the content differs per configuration.

export const environment = {
  production: true,
  apiUrl: 'https://api.example.com',
};

This bakes the API URL into the JavaScript bundle. That is fine for a value that never changes after build, and wrong for a value that should differ between a staging deploy and a production deploy of the identical build artifact, which the next section covers.

A mismatched pair, an environment.staging.ts file that does not exist yet, or a replace path with a typo, fails the build with a file-not-found error naming the missing path rather than silently falling back to the default environment.ts. That is worth knowing before assuming a build that succeeds is definitely pointing at the intended environment file; a successful build only confirms the referenced files exist, not that their contents are correct.

Budgets: catching bundle growth before it ships

The budgets array sets warning and error thresholds on bundle size, by type: initial for the main bundle, anyComponentStyle for a single component’s CSS, and a few others. Exceeding maximumError fails the build, which is the point, it catches a dependency that silently doubled the bundle before it reaches production.

A budget only in the production configuration is normal; development builds are unminified by design and would fail a production-sized budget immediately.

The failure message names the exact budget type and the size it exceeded by, which is usually enough to find the cause without a bundle analyzer: a newly added dependency that was not tree-shaken correctly, or a component style that imported an entire icon set instead of the handful of icons actually used. Raising maximumError to make the build pass again treats the symptom, not the bundle; it is sometimes the right call and sometimes a decision worth a second look first.

outputPath and baseHref for a subpath deploy

outputPath is where the built files land, dist/<project-name> by default, and it is what gets uploaded to a static host, not the src/ directory. baseHref matters only when the app is not served from the domain root: an app served at example.com/app/ needs --base-href=/app/ so that Angular’s router and asset URLs resolve against the right prefix, otherwise every route request 404s past the first page load.

ng build --configuration=production --base-href=/app/

What to check when a subpath deploy loads a blank page: view the page source in the browser and look at the generated script and stylesheet tag paths. If they point at /main.js instead of /app/main.js, the baseHref was not set for that build, and the browser is requesting assets from the domain root where nothing exists. The fix is rebuilding with the correct --base-href, not editing the deployed index.html by hand, since every asset reference in it depends on the same value.

Build-time config vs runtime config for the API URL

fileReplacements bakes the API URL into the bundle at build time, which means a different bundle per environment. The alternative is a small config.json fetched at app startup, read once in APP_INITIALIZER, which lets the same build artifact run against staging or production depending only on where it is deployed.

Build-time config is simpler and is the right default for a small app with one environment per build pipeline. Runtime config earns its complexity once the same artifact needs to be promoted from staging to production without a rebuild, which matters more the larger the deploy pipeline gets.

A second scenario where runtime config wins outright: a component library or embeddable widget built once and dropped into several unrelated sites, each needing its own API endpoint. Baking one URL into the bundle would mean building the artifact once per consuming site, which defeats the point of building it once at all; a runtime config.json fetched from wherever the widget is embedded keeps a single build usable everywhere.

Mapping the output to a static host

The dist/<project> folder is a set of static files plus an index.html. A static host serving Angular correctly needs one behaviour Angular does not configure itself: every unknown path should fall back to index.html so the Angular router, not the host, resolves client-side routes. Without that fallback, a hard refresh on /settings returns a 404 from the host before Angular ever runs.

esbuild-based Angular CLI (the application builder, default from Angular 17) speeds up the build and changes some internal output structure, output now lands directly under outputPath rather than a browser subfolder in older configurations, but the SPA fallback requirement and baseHref behaviour are unchanged from the older webpack-based builder.

What to check if routes work on first load but break on refresh: that is the exact symptom of a missing SPA fallback, since the first load always hits index.html directly regardless of host configuration, and only a direct request to a nested route exposes whether the host is rewriting unknown paths correctly.

CI usage

A CI build is the same commands run non-interactively, with the configuration name set by the branch or environment rather than typed by hand.

npm ci
ng build --configuration=production
# deploy dist/app to the static host

npm ci rather than npm install matters here: it installs exactly what the lockfile specifies and fails if package.json and the lockfile disagree, instead of silently updating dependencies mid-pipeline. A CI build that passes locally with npm install and fails in the pipeline with npm ci is usually telling the truth about an out-of-date lockfile, not producing a flaky false negative.

Mapping a branch to a configuration is typically a small conditional in the pipeline file: main builds with --configuration=production, everything else with --configuration=staging, so the same workflow file handles every branch without hand-editing the command per deploy.

A build that succeeds in CI but produces a different bundle than a local build usually traces back to a Node or npm version mismatch between the two environments, not the configuration itself. Pinning the Node version in the pipeline file to match what package.json’s engines field expects removes that class of difference before it has a chance to show up as an intermittent, hard-to-reproduce bug.

How this fits the rest of the stack

Once the build produces dist/app, the deploy question is entirely about the host: does it serve that folder with an SPA fallback so client-side routes survive a refresh, and does it apply the right base href if the app sits on a subpath. Static hosting on RunxBuild builds from the repository, applies SPA fallback automatically, and handles custom domains and certificates once the routing is right, with 120GB of bandwidth included before usage-based pricing applies, which the RunxBuild hosting calculator lays out for the traffic an Angular app actually generates.

Useful related references:

FAQ

What is the difference between ng build and ng build —configuration=production?

None since Angular 12. The CLI sets production as the default configuration on the build target, so a bare ng build already applies the production configuration unless angular.json has been changed to default elsewhere. —configuration=production is explicit but not required.

How do I set a different environment per build in Angular?

Use fileReplacements inside a named configuration in angular.json to swap environment.ts for a per-configuration file at build time, or fetch a config.json at app startup with APP_INITIALIZER if the same build artifact needs to run against different environments without a rebuild.

Where does ng build output go?

To the outputPath set in angular.json, dist/ by default. That folder, not the src directory, is what gets deployed to a static host.

Why does my Angular app 404 on a page refresh after deploying?

The static host is not falling back to index.html for unknown paths. Angular’s client-side router needs every path to serve index.html so it can take over and resolve the route in the browser; without that fallback rule, a direct request to a route path hits the host, not Angular.

Do I need —base-href when deploying an Angular app?

Only if the app is served from a subpath rather than the domain root. Set —base-href to match that subpath so the router and generated asset URLs resolve correctly; an app at the domain root does not need it set explicitly.

#angular ng build configuration#angular.json#ng build production#angular spa deploy#angular environments