'404 Error on page refresh with Angular 7, NGINX and Docker

I have an application written in Angular 7 that I am deploying to a Docker container with NGINX. When I run the container, everything works perfectly except that if i Refresh the page in the browser (F5) I get an NGINX 404 error page.

Here is my nginx.conf file from which you can see ive tried "try_files"

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    include /etc/nginx/conf.d/*.conf;

    server {
        listen 80; 

        location / {
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri /index.html;
        }
    }
}

My Dockerfile:

FROM node:alpine as builder
RUN apk update && apk add --no-cache make git

WORKDIR /app

COPY package.json package-lock.json /app/
RUN cd /app && npm install

COPY .  /app
RUN cd /app && npm run build

FROM nginx:alpine

RUN rm -rf /usr/share/nginx/html/* && rm -rf /etc/nginx/nginx.conf
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist/hyper-client-admin /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Directory on the deployed container is:

/usr/share/nginx/html # ls -la
total 23564
drwxr-xr-x    1 root     root          4096 May 20 00:18 .
drwxr-xr-x    1 root     root          4096 Mar  8 03:05 ..
drwxr-xr-x    2 root     root          4096 May 20 00:18 assets
-rw-r--r--    1 root     root        290728 May 20 00:18 es2015-polyfills.js
-rw-r--r--    1 root     root        211178 May 20 00:18 es2015-polyfills.js.map
-rw-r--r--    1 root     root           997 May 20 00:18 favicon.png
-rw-r--r--    1 root     root           770 May 20 00:18 index.html
-rw-r--r--    1 root     root        114749 May 20 00:18 main.js
-rw-r--r--    1 root     root        115163 May 20 00:18 main.js.map
-rw-r--r--    1 root     root        241546 May 20 00:18 polyfills.js
-rw-r--r--    1 root     root        240220 May 20 00:18 polyfills.js.map
-rw-r--r--    1 root     root          6224 May 20 00:18 runtime.js
-rw-r--r--    1 root     root          6214 May 20 00:18 runtime.js.map
-rw-r--r--    1 root     root       1117457 May 20 00:18 styles.js
-rw-r--r--    1 root     root       1191427 May 20 00:18 styles.js.map
-rw-r--r--    1 root     root      10048515 May 20 00:18 vendor.js
-rw-r--r--    1 root     root      10505601 May 20 00:18 vendor.js.map

And here is the console output:

172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET / HTTP/1.1" 200 371 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"lopment\hyper-client-admin>
172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET /runtime.js HTTP/1.1" 200 6224 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET /polyfills.js HTTP/1.1" 200 241546 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET /main.js HTTP/1.1" 200 114749 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET /styles.js HTTP/1.1" 200 1117457 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:30 +0000] "GET /vendor.js HTTP/1.1" 200 10048515 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:31 +0000] "GET /assets/logo-white.svg HTTP/1.1" 200 4519 "http://localhost:81/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:31 +0000] "GET /favicon.png HTTP/1.1" 200 997 "http://localhost:81/login" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
172.17.0.1 - - [20/May/2019:00:18:35 +0000] "GET /login HTTP/1.1" 404 188 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36" "-"
2019/05/20 00:18:35 [error] 6#6: *4 open() "/usr/share/nginx/html/login" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /login HTTP/1.1", host: "localhost:81"

Any ideas whats going on here?

UPDATE: The actual answer to this lies in the comments of @Rajesh's Answer. The issue is that I was working on /etc/nginx/nginx.conf and I needed to be working on /etc/nginx/conf.d/default.conf



Solution 1:[1]

this likely can be fixed quickly by simply using the useHash: true flag. For some unknown reason angular does not default this setting to true.

Make sure your app-routing-module.ts file contains useHash like this:

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],
  exports: [RouterModule]
})

Solution 2:[2]

I have an experience to add to this, I faced this recently with one of my applications where none of this seems to work, and it make me stumble upon records that the Nginx is slightly different with each release.

In my case, what solved the issue was a 404 fallback of the form below in the /etc/nginx/conf.d/default.conf due to inherent war my nginx.conf was written.

Below is the default.conf

server {
    listen       80;
    server_name  localhost;
    location / {
        root   /usr/share/nginx/html;
        try_files $uri $uri/ /index.html =404;
        index  index.html index.htm;
    }
}

nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    //This here takes the deafaut.conf alongside
    //possibly overwriting the server markings in the nginx.conf

    include /etc/nginx/conf.d/*.conf; 

    sendfile        on;

    keepalive_timeout  65;

    gzip  on;

    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            try_files $uri $uri/ /index.html =404;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

    }
}

You can typically remove the line to not take into consideration the default.conf or any other files and that would be fine for most dockerized containers(not in my case though!) and seems to work for many other scenarios.

Solution 3:[3]

In my case, I have one NGINX fronting a group of NGINX' as a proxy, and I had to add =404 to avoid a redirect loop:

location / {
    root        /usr/share/nginx/html;
    index       index.html;
    try_files   $uri $uri/ /index.html =404;
}

Solution 4:[4]

The answer of Rajesh is great, but I think it should be extended with some code.

With the current answer if, for example, a javascript file can't be found, the index.html is returned instead, which can lead the browser to cache the index.html file under the name of the javascript file you requested. If this happens your app will most likely crash and you need to clear the cache to get it working again.

To prevent this you can add a location block for the javascript files that will return a 404 if this file isn't found. See code below.

server {
    listen       80;
    server_name  localhost;
    root   /usr/share/nginx/html;

    
    # Add additional extension if you need it 
    location ~ \.(js)$ {
      try_files $uri $uri/ =404;
    }

    location / {
        try_files $uri $uri/ /index.html;
        index  index.html index.htm;
    }
}

Note: This example is with javascript files but you can extend the regex in the location block with any file extensions you want to have the 404 behaviour for when the file isn't found.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Rick
Solution 2 Abhimanyu Singh
Solution 3 yglodt
Solution 4 R. de Ruijter