I have come across multiple scenarios in my work project, where the angular application had to be deployed into the root or a subfolder within the root folder.
Configuring the Nginx web server to correctly serve the static assets like images or any other file and the index.html was always a big struggle, especially when deploying the app to a subfolder.
In this story, would like to share some useful points with an example and how base href plays a very important role.
Lets begin!
I have created a simple angular project nginxDemo.
Just have an AppComponent, where we are displaying a <h4> tag and an image.
<h4>Nginx Demo</h4>
<img src="assets/nginx.png">The image nginx.png is stored within the src/assets folder.

Below is the content of the index.html file. Observe the <base href>. It is set to "/". <base href> specifies the base URL for all relative URL's in the document.
I have created a Dockerfile with the below contents. Its straightforward. We are deploying the angular app to /usr/share/nginx/html.
Created a docker-compose.yml with the below contents. The docker container runs on port 80 and the application will be accessed on localhost:8081 in the browser.
Finally lets get to the nginx/nginx.config file in the root of the project. The nginx webserver will look for any file requests within the root directive. The root directive bears the value /usr/share/nginx/html. We already know that the angular app will be deployed to /usr/share/nginx/html. So here, the root directive location and the deployment folder is the same.
Lets now build the docker image, run the image to create a container.
Executing "docker compose build"
PS C:\Users\User\angular\nginxDemo> docker compose build
[+] Building 74.1s (17/17) FINISHED docker:default
=> [nginx-demo internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [nginx-demo internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 1.85kB 0.0s
=> [nginx-demo internal] load metadata for docker.io/library/nginx:alpine 8.4s
=> [nginx-demo internal] load metadata for docker.io/library/node:alpine 6.3s
=> [nginx-demo node 1/7] FROM docker.io/library/node:alpine@sha256:577f8eb599858005100d84ef3fb6bd6582c1b6b17877a393cdae4bfc9935f068 0.1s
=> => resolve docker.io/library/node:alpine@sha256:577f8eb599858005100d84ef3fb6bd6582c1b6b17877a393cdae4bfc9935f068 0.1s
=> [nginx-demo stage-1 1/4] FROM docker.io/library/nginx:alpine@sha256:31bad00311cb5eeb8a6648beadcf67277a175da89989f14727420a80e2e76742 0.1s
=> => resolve docker.io/library/nginx:alpine@sha256:31bad00311cb5eeb8a6648beadcf67277a175da89989f14727420a80e2e76742 0.1s
=> [nginx-demo internal] load build context 6.8s
=> => transferring context: 2.01MB 5.6s
=> CACHED [nginx-demo node 2/7] WORKDIR /app 0.0s
=> CACHED [nginx-demo node 3/7] COPY package.json package-lock.json ./ 0.0s
=> CACHED [nginx-demo node 4/7] RUN npm cache clean — force 0.0s
=> CACHED [nginx-demo node 5/7] RUN npm install 0.0s
=> [nginx-demo node 6/7] COPY . . 24.0s
=> [nginx-demo node 7/7] RUN npm run build 34.3s
=> CACHED [nginx-demo stage-1 2/4] RUN rm -r /usr/share/nginx/html/* 0.0s
=> CACHED [nginx-demo stage-1 3/4] COPY — from=node /app/dist/nginx-demo /app/sub 0.0s
=> [nginx-demo stage-1 4/4] COPY nginx/nginx.config /etc/nginx/conf.d/default.conf 0.1s
=> [nginx-demo] exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:8443b1fa3e7aca902b114b201c08e0023bc323bc04a52419af46733384232d54 0.0s
=> => naming to docker.io/library/testing-nginx 0.0s
PS C:\Users\User\angular\nginxDemo>Executing "docker compose up". Our nginx webserver is up and running.

Checking the docker container filesystem. The angular build output is deployed to the /usr/share/nginx/html folder.


Opening localhost:8081 in the browser.

Lets check how the below location directive executes:
location / {
# matches any path within the root
try_files $uri $uri/index.html =404;
autoindex on;
}
error_page 404 /index.html;The root directive location is /usr/share/nginx/html.
Observe the request URL highlighted in the screenshot above. It is localhost:8081/. $uri is /.
=>try_files directive will first check if there is a file by name $uri within the root directive location. If yes serve it. In our example, no such file found.
=>Since first check has failed, try_files directive will look for an index.html file within the $uri folder. We do have an index.html within the "/" i.e we do have an index.html within /usr/share/nginx/html/. Thus the index.html will served.
Lets take an example of how the *.css file will be served.
In the screenshot below, the request URL is http://localhost:8081/styles.ef46db3751d8e999.css. $uri is /styles.ef46db3751d8e999.css.
try_files directive will first check if this file is present in the root directive location. Yes it is found ! So try_files directive will not do any further checks.

The request URL in the below screenshot is http://localhost:8081/assets/nginx.png. $uri will be /assets/nginx.png.
try_files directive will first check if a file by name $uri exists within the root directive location. Yes we do have nginx.png within the assets folder inside /usr/share/nginx/html. So try_files serves it with no further checks.

The scenario we saw now is the simplest one. The app is deployed to the root of the /usr/share/nginx/html.
Deploying angular application to a subfolder
Angular app is deployed to a subfolder sub within /usr/share/nginx/html
Let me modify the below COPY instruction within the Dockerfile to deploy the angular app to a folder sub within /usr/share/nginx/html
FROM
COPY — from=node /app/dist/nginx-demo /usr/share/nginx/htmlTO
COPY — from=node /app/dist/nginx-demo /usr/share/nginx/html/subNo further changes made. Now lets again execute "docker compose build" and "docker compose up".
Checking the docker container filesystem. The angular build output is now deployed to /usr/share/nginx/html/sub folder.

Nginx root directive location is /usr/share/nginx/html.
Hitting localhost:8081 in the browser. As expected we have 404 errors.

From the above screenshot, the request URL is localhost:8081/ and $uri is /.
=> try_files directive will look for a file by name $uri within the root directive location. No such file is found.
=>try_files directive performs another check. It looks for the index.html file within the $uri folder. This check too fails because index.html is not present within /usr/share/nginx/html/. Hence 404 error is returned.
How do we solve this problem ?
If we are deploying the angular app to /usr/share/nginx/html/sub, we have 3 solutions:
Solution-1
Use a nginx rewrite directive to rewrite all request URLS to include /sub/ before the resource name. Example :http://localhost:8081/*.css will be internally rewritten to http://localhost:8080/sub/*.css
Application will be accessed on http://localhost:8081/sub/ and will work as expected. The request URLS in the network tab will show no sign of /sub/. Everything happens behind the scenes.
Solution-2
=>The application needs to be accessed on http://localhost:8081/sub/ so that nginx looks for the index.html file inside a folder sub which in turn is inside the root directive location(i.e /usr/share/nginx/html).
=> We need to modify the requests of the *.css or *.js or any static assets from
http://localhost:8081/*.css to http://localhost:8080/sub/*.css
http://localhost:8081/*.js to http://localhost:8080/sub/*.js
http://localhost:8081/assets/*.png to http://localhost:8080/sub/assets/*.png
This will ensure nginx looks for these files inside the sub folder within the root directive location(/usr/share/nginx/html). To achieve this, we can use the base href to our advantage. This means the base href in the index.html needs to be modified from <base href="/"> to <base href="/sub/">
Solution-3
Ensure the nginx root directive location matches the location where the angular app is deployed.
This means if the angular app is deployed to /usr/share/nginx/html/sub then the root directive in the nginx config must also be /usr/share/nginx/html/sub.
No need for updating the <base href="/">. Nginx will always look into the deployment folder location for any resources to be fetched.
I. Implementing Solution-1
The nginx root directive location remains /usr/share/nginx/html. The <base href> remains "/" in the index.html.
We just have added an additional rewrite directive in the nginx.config file.
rewrite ^/(.*.css|js|png)$ /sub/$1;We are rewriting any request for css or js or png files. Below is an example:
http://localhost:8081/styles.ef46db3751d8e999.css will be internally rewritten to http://localhost:8081/sub/styles.ef46db3751d8e999.css. You wouldn't see any change in the request URL in the browser network tab.
The final nginx.config looks like below. What has changed is the additional rewrite directive in line 17.
Hitting localhost:8081/sub/ in the browser. The request URL in the below screenshot is http://localhost:8081/sub/. The $uri is /sub/.
=>The try_files directive first looks for a file by name $uri within the root directive location. The check fails.
=>The try_files directive then looks for an index.html file within a folder by name $uri which in turn is within the root directive location. This check passes. Its able to find the index.html within /usr/share/nginx/html/sub/ and serves the file.




Please observe the highlighted lines in the docker container logs below to see how the rewrite directive matches the filename against the regex expression we provided and rewrites the request internally if the request URL matches the regex expression.


II. Implementing Solution-2
Root directive location remains /usr/share/nginx/html.
This solution is useful when you have multiple apps deployed to different subfolders and the same nginx server is used to serve those multiple apps.
Please check the git repository below for such an example, where we have a multi-project workspace. There are 2 angular apps in a single workspace. We have deployed the apps to different subfolders and running it inside a single docker container. This example will explain how <base href> and nginx helps us achieve our objective.
Both applications run on localhost:8082. Navigating to localhost:8082/first/ takes me to 1 application and navigating to localhost:8082/second/ takes me to the 2nd application.


Back to our original example,
If I hit localhost:8081/sub/ in the browser, observe that the index.html is successfully fetched from the sub folder within the /usr/share/nginx/html.

Lets review the request URL above. It is http://localhost:8081/sub/. $uri is /sub/ from the request URL.
=> try_files directive will first check if there is a $uri file within the root directive location. If yes, serve it. This check has failed.
=>try_files directive will now check for the presence of index.html within the $uri folder. This check will pass because we do have an index.html within the sub folder inside the root directive location.
What about the other files ? They have all failed with 404 error.

http://localhost:8081/styles.ef46db3751d8e999.css is the request URL above.The $uri will be /styles.ef46db3751d8e999.css.
=> try_files directive will first check if there is a file by name $uri within the root directive location. This check will fail.
=>try_files directive will now check if it can find an index.html within the folder by name $uri. Since there is no such folder within the root directive location , there can be no index.html within the non-existent folder as well. Hence nginx has returned a 404 error.
To fix the 404 errors for the *.css,*.js files and other static assets, lets modify the <base href> in the index.html
FROM
<base href="/">TO
<base href="/sub/">Executing "docker compose build" and "docker compose up".
Hitting localhost:8081/sub/ in the browser, we are able to successfully load all the files.

Observe the change in the request URL above.
Updating the <base href> to "/sub/", has updated the request URL from http://localhost:8081/styles.ef46db3751d8e999.css to http://localhost:8081/sub/styles.ef46db3751d8e999.css. Similar change is observed in all other *.js and *.png files.
III. Solution-3.
Lets update the nginx root directive to be same as the folder where the angular application is deployed.
Update the root directive in the nginx.config
FROM
root /usr/share/nginx/html;TO
root /usr/share/nginx/html/sub;The <base href> in the index.html remains "/" and the root directive location is /usr/share/nginx/html/sub.
Executing "docker compose build" and "docker compose up".
Hitting localhost:8081 in the browser. All the files are successfully fetched since the root directive and the deployment folder are the same.



There is one interesting thing you can try at this point. This help you even better understand how the mechanism works. If you update the <base href> to "/sub/", observe how the request URL's change.
When you hit localhost:8081 in the browser, the request URL is http://localhost:8081/ and $uri is /.

=>try_files directive looks for a file by name $uri within the root directive location. This check will fail.
=>try_files directive will now check if there is an index.html within a folder by name $uri, which in turn is within the root directive location(i.e /usr/share/nginx/html/sub/.) The 2nd check will be successful.
Why has the *.css file failed with 404 error in the below screenshot ?

The request URL is http://localhost:8081/sub/styles.ef46db3751d8e999.css and $uri is /sub/styles.ef46db3751d8e999.css
=>try_files directive will first check for a file by name $uri in the root directive location. This means its looking for /sub/styles.ef46db3751d8e999.css within /usr/share/nginx/html/sub. We have styles.ef46db3751d8e999.css within /usr/share/nginx/html/sub, not within /usr/share/nginx/html/sub/sub. This check will fail.
=>try_files directive will do another check. It will look for index.html within a folder by name $uri, which in turn is within the root directive location.
This means its looking for index.html within /usr/share/nginx/html/sub/sub/styles.ef46db3751d8e999.css. This check, as expected will fail as well and a 404 error is returned.
Stackademic 🎓
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord
- Visit our other platforms: In Plain English | CoFeed | Venture | Cubed
- More content at Stackademic.com