Dockerize React with Typescript, ReactRouter and nginx as reverse proxy

I am currently building an architecture via Docker and Docker Compose.
In this case, I have two apps with the following setup:

enter image description here

I want these apps to be accessible via the following URL:
app1 frontend: http://localhost/app1
app1 backend: http://localhost/app1/backend

app2 frontend http://localhost/app2
app2 backend http://localhost/app2/backend

In the frontend of the apps I use React with Typescript and ReactRouter.
In the backend I use .NET 5 Core. These run under port 5000 and 5001 respectively.

Furthermore a reverse proxy in form of NGINX is used.

This works fine as long as the root URL is addressed in ReactRouter. A different path in the URL leads to a HTTP 404 which comes from NGINX.

ReactRouter in App1:

    import './App.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import Home from './components/home/Home';
import WeatherForeCastList from './components/weatherForeCastList/WeatherForeCastList';

function App() {
  return (
    <div className="App">
      <Router>
        <Switch>
          <Route path="/b">
            <WeatherForeCastList />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </Router>
    </div>
  );
}

export default App;

NGINX Config:

# auto detects a good number of processes to run
worker_processes auto;

#Provides the configuration file context in which the directives that affect connection processing are specified.
events {
    # Sets the maximum number of simultaneous connections that can be opened by a worker process.
    worker_connections 8000;
    # Tells the worker to accept multiple connections at a time
    multi_accept on;
}


http {
    # what times to include
    include       /etc/nginx/mime.types;
    # what is the default one
    default_type  application/octet-stream;

    # Sets the path, format, and configuration for a buffered log write
    log_format compression '$remote_addr - $remote_user [$time_local] '
        '"$request" $status $upstream_addr '
        '"$http_referer" "$http_user_agent"';

    upstream app1_backend {
        # Must be service name of docker compose file
        server app1-backend:5000;
    }

    upstream app2_backend {
        # Must be service name of docker compose file
        server app2-backend:5000;
    }

    server {
        # listen on port 80
        listen 80;
        # save logs here
        access_log /var/log/nginx/access.log compression;

        # where the root here
        root /usr/share/nginx/html;
        # what file to server as index
        index index.html index.htm;

        location /app1/backend/ {
            rewrite /app1/backend/(.*) /$1  break;
            proxy_pass http://app1_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }    

        location /app2/backend/ {
            rewrite /app2/backend/(.*) /$1  break;
            proxy_pass http://app2_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to redirecting to index.html
            try_files $uri $uri/ /index.html;
        }
    }
}

Docker-File app1 Frontend:

FROM node:12.18-alpine AS builder
ENV NODE_ENV=production
WORKDIR /app1/frontend
COPY . .
RUN rm -rf node_modules
RUN npm install --production --silent

RUN rm -rf build
RUN PUBLIC_URL=http://localhost/app1 npm run build --loglevel verbose

Docker-File app1 Backend:

FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal AS base
WORKDIR /app1/backend
#EXPOSE 5000

ENV ASPNETCORE_URLS=http://+:5000

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app1
USER appuser

FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build
WORKDIR /src
COPY ["backend.csproj", "."]
RUN dotnet restore "backend.csproj"
COPY . .

RUN dotnet build "backend.csproj" -c Release -o /app1/backend/build

FROM build AS publish
RUN dotnet publish "backend.csproj" -c Release -o /app1/backend/publish

FROM base AS final
WORKDIR /app1/backend
COPY --from=publish /app1/backend/publish .
ENTRYPOINT ["dotnet", "backend.dll"]

Docker Compose:

version: '3.4'

services:
  postgres:
    build:
      context: ./db
      dockerfile: ./Dockerfile
    hostname: postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_MULTIPLE_DATABASES=app1,app2
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped
  
  pgadmin:
    image: dpage/pgadmin4
    ports:
      - 5555:80
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: postgres
    restart: unless-stopped
    volumes:
      - pgadmin-data:/var/lib/pgadmin
    depends_on:
      - postgres

  app1-backend:
    image: app1-backend
    build:
      context: ./app1/backend
      dockerfile: ./Dockerfile
    depends_on:
      - postgres

  app1-frontend:
    image: app1-frontend
    build:
      context: ./app1/frontend
      dockerfile: ./Dockerfile
    environment:
      NODE_ENV: production
      PUBLIC_URL: http://localhost/app1
    stdin_open: true
    volumes:
      - app1-frontend-build:/app1/frontend/build
    depends_on:
      - app1-backend
  
  app2-backend:
    image: app2-backend
    build:
      context: ./app2/backend
      dockerfile: ./Dockerfile
    depends_on:
      - postgres

  app2-frontend:
    image: app2-frontend
    build:
      context: ./app2/frontend
      dockerfile: ./Dockerfile
    environment:
      NODE_ENV: production
      PUBLIC_URL: http://localhost/app2
    stdin_open: true
    volumes:
      - app2-frontend-build:/app2/frontend/build
    depends_on:
      - app2-backend

  router:
    image: router
    build: 
      context: ./router
      dockerfile: ./Dockerfile
    ports:
      - 80:80
    volumes:
      - app1-frontend-build:/usr/share/nginx/html/app1
      - app2-frontend-build:/usr/share/nginx/html/app2
    depends_on: 
      - app1-frontend
      - app2-frontend
    
volumes:
    postgres-data:
    pgadmin-data:
    app1-frontend-build:
    app2-frontend-build:

Does anyone here have any idea why the URL:
http://localhost/app1/ works

but the URL:
http://localhost/app1/b does not (404 from NGINX)?

Thanks for your effort and please let me know if you need further information!

Source: Docker Questions

LEAVE A COMMENT