September 2, 2026

Build Once, Deploy Everywhere: Runtime-Configured Frontend Delivery

By
Joe Kallas
Senior Software Engineer
BG
Photo by
James Harrison on Unsplash

Build Once, Deploy Everywhere: Runtime-Configured Frontend Delivery

A few months ago, our frontend team ran into a problem that looked small at first but had a much bigger impact on our delivery process.

A client’s application had separate builds for staging, QA, and production. Every environment required its own build command, its own configuration file, and its own deployment artifact. Nothing was obviously broken, but every promotion introduced a question we didn’t want to answer: Are we really deploying the exact same application that was tested?

The source code was identical, but the generated applications were not. Each build happened independently, at different times, with different environment variables injected during compilation. That difference created unnecessary uncertainty in the release process.

The traditional frontend deployment model relies on environment-specific build scripts defined in package.json, such as build:staging and build:production. Each script points to a different .env file, and the bundler replaces those values during compilation, embedding them directly into the JavaScript bundle.

This approach works, but it creates a tight connection between the build process and the target environment. A staging build becomes a staging artifact. A production build becomes a separate production artifact.

As more environments are introduced (QA, pre-production, canary releases, etc.) the complexity grows. Teams end up managing additional build scripts, configuration files, Docker images, and promotion steps. Every environment change requires another build, increasing the chance of differences between what was tested and what is eventually released.

The approach we adopted was runtime configuration injection: build the application once, then provide environment-specific values when the application starts rather than during compilation.

Moving configuration from build time to runtime

The idea is simple: the frontend bundle should not need to know where it will eventually run.

A file called runtimeconfig.js is included in the application's static assets. During the initial build, it acts as a placeholder containing an empty window.config object. Its purpose is to guarantee that the application has a predictable location from which to read runtime configuration.

When the application is deployed, that file is populated with the correct environment values. Since it is loaded before the application entry point, any component that needs configuration can access the values immediately.

A small helper function, getConfig, reads from window.config first and falls back to import.meta.env (or process.env, depending on the bundler) for local development. This keeps the local developer workflow unchanged while allowing deployments to share the same build artifact.

For additional safety, configuration keys are defined separately with type information, allowing missing or invalid keys to be detected during development rather than after deployment.

Managing runtime values

The configuration values are stored in AWS Parameter Store as JSON. These values are runtime settings that vary between environments, such as API endpoints or feature flags.

During the deployment process, a script defined in buildspec.yaml retrieves the required parameters, converts them into the expected format, and writes them into runtimeconfig.js before the application artifacts are published.

For teams deploying Docker images directly rather than using an automated CodeBuild pipeline, the same pattern can be implemented through an entrypoint.sh script. The script runs when the container starts, retrieves environment values from the available source (such as Docker Compose variables or the host environment), and updates runtimeconfig.js before the application becomes available.

In both cases, the frontend container remains unchanged. The difference between environments comes only from the configuration provided at runtime.

Serving the application

The container runs Nginx, which serves the generated frontend files and handles incoming browser requests.

After deployment, CloudFront distributes those static assets closer to users around the world. JavaScript and CSS files use content-based filenames, allowing them to be cached efficiently while still updating safely when a new version is deployed. When required, cache invalidation ensures that updated assets become available without waiting for previous cached versions to expire.

The deployment flow becomes:

  1. Code changes trigger the build pipeline.
  1. A single Docker image is created with the frontend application and Nginx.
  1. Runtime configuration values are injected during deployment.
  1. The same artifact is promoted across environments with different runtime settings.
  1. Updated assets are distributed to users.
The impact

For our team, this approach removed a source of uncertainty from the release process. Instead of creating slightly different applications for each environment, we could validate one build and move that same artifact forward through the pipeline.

The result was a simpler promotion process, fewer environment-specific build steps, and greater confidence that what was tested was the same thing that reached users.

Take the first step toward building your dream team

Book a call and get matched with engineers in 24–72h.