Search code examples
reactjsdockerkubernetescreate-react-appminikube

How do you get subpaths to work for create-react-app in minikube?


Figured this would be straight forward, but apparently this is not the case.

I have two CRA microservices running at:

  • /
  • /admin

minikube ip is 192.168.64.5, so the full routes should be 192.168.64.5/ and 192.168.64.5/admin in browser. The Dockerfile.dev just has each being started with npm start.

The root route works fine, but the /admin I can't get the static files to serve correctly for the life of me... spent several hours on it. All I get back is Uncaught SyntaxError: Unexpected token '<', which stems from the static files not being served correctly.

That being said, when I do npm build, or just go to localhost:3000/admin, the web application is served correctly. npm build is obviously not ideal for development. Using localhost just circumvents the cluster routing, so issues that could have been caught in dev, will now show-up in staging. I'll have to go that route if I can't get this working though.

I've tried the "homepage:" and basename={process.env.PUBLIC_URL}, but it is not resolving the issue:

# package.json 

{
  "name": "admin",
  "version": "0.1.0",
  "private": true,
  "homepage": "/admin",
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "history": "^5.0.0",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

# index.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { createBrowserHistory } from "history";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

export const history = createBrowserHistory({
  basename: process.env.PUBLIC_URL
});

ReactDOM.render(
  <Router basename={process.env.PUBLIC_URL}>
    <Switch>
      <Route path="/">
        <App />
      </Route>
    </Switch>
  </Router>,
  document.getElementById("root")
);


serviceWorker.unregister();

# index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> -->
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <base href="%PUBLIC_URL%/">
    <!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <!-- <link rel="manifest" href="./manifest.json" /> -->
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

Also, tried with .env PUBLIC_URL, but has the same effect as "homepage":.

The documentation says:

Normally, Create React App ignores the hostname.

But apparently there is something it doesn't like about minikube ip and the cluster. Seems like this would be a pretty common requirememt, so there is probably a straight forward solution I'm just overlooking.

Any suggestions for getting this working?

# ingress.yaml

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
  name: ingress-service-dev
  namespace: default
spec:
  rules:
    - http:
        paths:
          - path: /admin/?(.*)
            backend:
              serviceName: admin-cluster-ip-service-dev
              servicePort: 4001
          - path: /?(.*)
            backend:
              serviceName: client-cluster-ip-service-dev
              servicePort: 3000
          - path: /api/?(.*)
            backend:
              serviceName: api-cluster-ip-service-dev
              servicePort: 5000

Solution

  • Ok, apparently the issue was nginx.ingress.kubernetes.io/rewrite-target: /$1 in the ingress.yaml. I commented it out and it started working.

    You can refer to the index.html, index.js, and package.json above as those are unchanged.

    The final ingress.yaml I have is:

    # ingress.yaml
    
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      annotations:
        kubernetes.io/ingress.class: "nginx"
      name: ingress-service-dev
      namespace: default
    spec:
      rules:
        - http:
            paths:
              - path: /admin/
                backend:
                  serviceName: admin-cluster-ip-service-dev
                  servicePort: 4001
              - path: /api/
                backend:
                  serviceName: api-cluster-ip-service-dev
                  servicePort: 5000
              - path: /
                backend:
                  serviceName: client-cluster-ip-service-dev
                  servicePort: 3000
    

    Nothing breaking so far and everything working as expected.