<h3>Intro</h3>
<p>I know this question has been asked in many different forms. But I have already read every post and article I could find on that matter and could not solve my problem with the prerequisites we face. Therefore I would like to describe our problem first and give our motivation, and thereafter describe the different methods we already tried or am using, even though they are unsatisfactory.</p>
<h3>Problem/Motiviation</h3>
<p>Our setup could be described as somewhat complex. Our Angular application is deployed on several Kubernetes clusters and served from different paths.</p>
<p>The clusters themselves are behind proxies and Kubernetes itself adds a proxy named Ingress itself. Thus a common setup for us may look like the following:</p>
<p><strong>Local</strong></p>
<p>http://localhost:4200/</p>
<p><strong>Local Cluster</strong></p>
<p><a href="https://kubernetes.local/angular-app" rel="noreferrer">https://kubernetes.local/angular-app</a></p>
<p><strong>Development</strong></p>
<p><a href="https://ourcompany.com/proxy-dev/kubernetes/angular-app" rel="noreferrer">https://ourcompany.com/proxy-dev/kubernetes/angular-app</a></p>
<p><strong>Staging</strong></p>
<p><a href="https://ourcompany.com/proxy-staging/kubernetes/angular-app" rel="noreferrer">https://ourcompany.com/proxy-staging/kubernetes/angular-app</a></p>
<p><strong>Customer</strong></p>
<p><a href="https://kubernetes.customer.com/angular-app" rel="noreferrer">https://kubernetes.customer.com/angular-app</a></p>
<p>The application is always served with a different base-path in mind. As this is Kubernetes, our app is deployed with Docker-containers.</p>
<p>Classically, one would use <code>ng build</code> to transpile the Angular app and then use e.g. a Dockerfile like the following:</p>
<pre><code>FROM nginx:1.17.10-alpine

EXPOSE 80

COPY conf/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY dist/ /usr/share/nginx/html/
</code></pre>
<p>To tell the CLI how to set the base-href and where assets are served from one would use <code>--base-href</code> and <code>--deploy-url</code> options of the Angular CLI.</p>
<p>This unfortunately means, that we need to build a Docker-container for every environment, in which we want to deploy the application.</p>
<p>Furthermore, we have to take care in this prebuilding process to set other environment variables e.g. inside the <code>environments[.prod].ts</code> for every deployment environment.</p>
<h3>Current Solution</h3>
<p>Our current solution comprises of building the app therefore during the deployment. That works by using a different Docker-container used as a so called <em>initContainer</em> which runs before the nginx-container is started.</p>
<p>The Dockerfile for this one looks as follows:</p>
<pre><code>FROM node:13.10.1-alpine

# set working directory
WORKDIR /app

# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH

# install and cache app dependencies
COPY package*.json ./

RUN npm install

COPY / /app/

ENV PROTOCOL http
ENV HOST localhost
ENV CONTEXT_PATH /
ENV PORT 8080
ENV ENDPOINT localhost/endpoint
VOLUME /app/dist

# prebuild ES5 modules
RUN ng build --prod --output-path=build
CMD \
  sed -i -E &quot;s@(endpoint: ['|\&quot;])[^'\&quot;]+@\1${ENDPOINT}@g&quot; src/environments/environment.prod.ts \
  &amp;&amp; \
  ng build \
  --prod \
  --base-href=${PROTOCOL}://${HOST}:${PORT}${CONTEXT_PATH} \
  --deploy-url=${PROTOCOL}://${HOST}:${PORT}${CONTEXT_PATH} \
  --output-path=build \
  &amp;&amp; \
  rm -rf /app/dist/* &amp;&amp; \
  mv -v build/* /app/dist/
</code></pre>
<p>Incorporating this container into a Kubernetes deployment as an <em>initContainer</em> allows the app to be built with the correct base-href, deployment-url and that specific variables are replaced depending on the environment this app is deployed in.</p>
<p>This approach works perfectly except for a few problems it creates:</p>
<ol>
<li>One deployment takes several minutes<br>
The <em>initContainer</em> needs to run every time the app is redeployed. This always runs the <code>ng build</code> command of course. To prevent it to build the ES-modules every time the container already runs the <code>ng build</code> command in the prior <code>RUN</code> directive to cache these.<br>
Nevertheless, the building for differential loading and the Terser etc. are running again which takes several minutes until completion.<br>
When there is a horizontal pod autoscaling then it would take forever for the additional pods to be available.</li>
<li>The container contains the code. This is more a policy but it is discouraged in our company to deliver the code with a deployment. At least not in an non-obfuscated form.</li>
</ol>
<p>Because of those two issues we decided to move the logic away from Kubernetes/Docker directly into the app itself.</p>
<h3>Planned Solution</h3>
<p>After some research we stumbled on the <code>APP_BASE_HREF</code> InjectionToken. Thus we tried to follow different guides on the web to dynamically set this depending on the environment the app is deployed in. What was concretely first done is this:</p>
<ol>
<li>add a file named <code>config.json</code> in <code>/src/assets/config.json</code> with the base-path in the content (and other variables)</li>
</ol>
<pre><code>{
   &quot;basePath&quot;: &quot;/angular-app&quot;,
   &quot;endpoint&quot;: &quot;https://kubernetes.local/endpoint&quot;
}
</code></pre>
<ol start="2">
<li>We added code to the <code>main.ts</code> file that probes the current <code>window.location.pathname</code> recursively through the parent history to find the <code>config.json</code>.</li>
</ol>
<pre class="lang-js prettyprint-override"><code>export type AppConfig = {
  basePath: string;
  endpoint: string;
};

export const APP_CONFIG = new InjectionToken&lt;AppConfig&gt;('APP_CONFIG');

export async function fetchConfig(): Promise&lt;AppConfig&gt; {
  const pathName = window.location.pathname.split('/');

  for (const index of pathName.slice().reverse().keys()) {

    const path = pathName.slice(0, pathName.length - index).join('/');
    const url = stripSlashes(`${window.location.origin}/${path}/assets/config.json`);
    const promise = fetch(url);
    const [response, error] = await handle(promise) as [Response, any];

    if (!error &amp;&amp; response.ok &amp;&amp; response.headers.get('content-type') === 'application/json') {
      return await response.json();
    }
  }
  return null;
}

fetchConfig().then((config: AppConfig) =&gt; {
  platformBrowserDynamic([{ provide: APP_CONFIG, useValue: config }])
    .bootstrapModule(AppModule)
    .catch(err =&gt; console.error('An unexpected error occured: ', err));
});
</code></pre>
<ol start="3">
<li>Inside the <code>app.module.ts</code> the <code>APP_BASE_HREF</code> is initialized with the <code>APP_CONFIG</code></li>
</ol>
<pre class="lang-js prettyprint-override"><code>@NgModule({
  providers: [
  {
    provide: APP_BASE_HREF,
    useFactory: (config: AppConfig) =&gt; {
      return config.basePath;
    },
    deps: [APP_CONFIG]
  }
  ]
})
export class AppModule { }
</code></pre>
<p><strong>Important:</strong> we used this approach instead of using the <code>APP_INITIALIZER</code> because when we tried it the provider for the <code>APP_BASE_HREF</code> was always run before the provider for the <code>APP_INITIALIZER</code>, thus the <code>APP_BASE_HREF</code> was always undefined.</p>
<p>Unfortunately, this only worked locally and does not work while the app is proxied. The issue we have observed here is, that when the app is initially served by the webserver without specifying the base-href and the deploy-url, the app tries to load everything from '/' (root) obviously.</p>
<p>But this also means it tries to fetch the angular scripts aka <code>main.js</code>, <code>vendor.js</code>, <code>runtime.js</code> and all the other assets from there and so none of our code is actually run.</p>
<p>To fix this we adapted the code slightly.</p>
<p>Instead of letting angular probe the server for the <code>config.json</code> we placed the code directly inside the <code>index.html</code> and inlined it.<br>
Like this we could find the base-path and replace all the links in the html with the prefixed one, to load at least the scripts and other assets. This looks as follows:</p>
<pre class="lang-html prettyprint-override"><code>&lt;body&gt;
  &lt;app-root&gt;&lt;/app-root&gt;
  &lt;script&gt;
    function addScriptTag(d, src) {
      const script = d.createElement('script');
      script.type = 'text/javascript';
      script.onload = function(){
        // remote script has loaded
      };
      script.src = src;
      d.getElementsByTagName('body')[0].appendChild(script);
    }

    const pathName = window.location.pathname.split('/');

    const promises = [];

    for (const index of pathName.slice().reverse().keys()) {

      const path = pathName.slice(0, pathName.length - index).join('/');
      const url = `${window.location.origin}/${path}/assets/config.json`;
      const stripped = url.replace(/([^:]\/)\/+/gi, '$1')
      promises.push(fetch(stripped));
    }

    Promise.all(promises).then(result =&gt; {
      const response = result.find(response =&gt; response.ok &amp;&amp; response.headers.get('content-type').includes('application/json'));
      if (response) {
        response.json().then(json =&gt; {
          document.querySelector('base').setAttribute('href', json.basePath);
          for (const node of document.querySelectorAll('script[src]')) {
            addScriptTag(document, `${json.basePath}/${node.getAttribute('src')}`);
            node.remove();
            window['app-config'] = json;
          }
        });
      }
    });
  &lt;/script&gt;
&lt;/body&gt;
</code></pre>
<p>Furthermore we had to adapt the code inside the <code>APP_BASE_HREF</code> provider as follows:</p>
<pre class="lang-js prettyprint-override"><code>useFactory: () =&gt; {
  const config: AppConfig = (window as {[key: string]: any})['app-config'];
  return config.basePath;
},
deps: []
</code></pre>
<p>Now what happens is, that it loads the page, replaces the source urls with the prefixed ones, loads the scripts, loads the app and sets the <code>APP_BASE_HREF</code>.</p>
<p>Routing seems to work but all the other logic like loading language-files, markdown-files and other assets doesn't work anymore.</p>
<p>I think the <code>--base-href</code> option actually sets the <code>APP_BASE_HREF</code> but what the <code>--deploy-url</code> option does I could not find out.<br>
Most of the articles and posts specify that it is enough to specify the base-href and the assets would work as well, but that does not seem to be the case.</p>
<h3>Question</h3>
<p>Considering all that, my question then would be how to design an Angular app to definitely be able to set its base-href and deploy-url, so that all angular features like routing, translate, import() etc. are working as if I had set those via the Angular CLI?</p>
<p>I am not sure if I gave enough Information to fully comprehend what our problem is and what we expect, but if not I will provide it if possible.</p>

satanik
<p>It took some time but we finally managed to find an easy enough solution to cover all the points we made in the question.</p>
<p>The solution is quite close to the original approach of just building the Angular app offline and copying the content inside an <code>nginx</code> container, mixed with the build process of previously used in the init container.</p>
<p>Our solution then looks as follows. We first build the application offline and inject some recognisable string where the <code>base-href</code> and the <code>deploy-url</code> should be later.</p>
<pre class="lang-sh prettyprint-override"><code>ng build --prod --base-href=http://recognisable-host-name/ --deploy-url=http://recognisable-host-name/
</code></pre>
<p>The resulting <code>runtime*.js</code> and the <code>index.html</code> file will contain these inside the code and as the base for assets to load.</p>
<p>Then in the second phase the Dockerfile for a standard Angular application is adapted from</p>
<pre><code>FROM nginx:1.17.10-alpine

EXPOSE 80

COPY conf/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY dist/ /usr/share/nginx/html/
</code></pre>
<p>to the new version like this</p>
<pre><code>FROM nginx:1.17.10-alpine

EXPOSE 80

COPY conf/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY dist/ /usr/share/nginx/html/

ENV CONTEXT_PATH http://localhost:80
ENV ENDPOINT http://localhost/endpoint
ENV VARIABLE foobar

CMD \
  mainFiles=$(ls /usr/share/nginx/html/main*.js) \
  &amp;&amp; \
  for f in ${mainFiles}; do \
    envsubst '${ENDPOINT},${VARIABLE}' &lt; &quot;$f&quot; &gt; &quot;${f}.tmp&quot; &amp;&amp; mv &quot;${f}.tmp&quot; &quot;$f&quot;; \
  done \
  &amp;&amp; \
  runtimeFiles=$(grep -lr &quot;recognisable-host-name&quot; /usr/share/nginx/html) \
  &amp;&amp; \
  for f in ${runtimeFiles}; do sed -i -E &quot;s@http://recognisable-host-name/?@${CONTEXT_PATH}@g&quot; &quot;$f&quot;; done \
  &amp;&amp; \
  nginx -g 'daemon off;'
</code></pre>
<p>Here first we pass the substitutions we would like to make. First the <code>CONTEXT_PATH</code> is the full base path that will replace the <code>http://recognisable-host-name</code> string we injected before, by first finding all the files containing the string and then replacing the it with the <code>sed</code> command.</p>
<p>The other variables that may be environment specific can also be replaced by utilising <code>envsubst</code> and replacing all mentions of those variables from the <code>main*.js</code> files. Therefore the <code>environments.prod.ts</code> is prepared as follows:</p>
<pre class="lang-js prettyprint-override"><code>export const environment = {
    &quot;endpoint&quot;: &quot;${ENDPOINT}&quot;,
    &quot;variable&quot;: &quot;${VARIABLE}&quot;
}
</code></pre>
<p>This takes care of all the main points I raced, namely:</p>
<ol>
<li>do not share the original code</li>
<li>make a deployment fast</li>
<li>container can be served behind any proxy (e.g. Nginx-Ingress/Kubernetes)</li>
<li>environment variables can be injected</li>
</ol>
<p>I hope it helps somebody else, especially because as I was researching this topic I found dozens of articles but none met all the criteria or necessitated lots of custom code that is complex or inefficient or hard to maintain.</p>
<p><strong>Edit:</strong></p>
<p>If somebody is interested, I set up a demo project where this solution can be tested:
<br>
<a href="https://gitlab.com/satanik-angular/base-path-problem" rel="nofollow noreferrer">https://gitlab.com/satanik-angular/base-path-problem</a></p>
