Build GitHub Actions deploy Heroku React app and Express Server

I have a React app served by an Express server and have set up GitHub Actions to handle the build on GitHub due to memory issues on Heroku. Previously, I was building the app directly on Heroku, and it worked fine, but I started hitting memory limits, so I switched the build process to GitHub Actions.

Folder Structure:

├── .github
├── client
├── src
├── .gitignore
├── Procfile
├── server.js
└── worker.js

GitHub Actions Workflow:

In GitHub Actions, I build the client and then deploy to Heroku.

deploy.yml:

name: Build and Deploy to Heroku

on:
  push:
    branches:
      - staging  # Change this if your default branch is different

jobs:
  build-and-deploy:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3

      - name: Set up Node.js (Server)
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          cache: 'npm'

      - name: Install Server Dependencies
        run: npm ci

      - name: Set up Node.js (Client)
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          cache: 'npm'

      - name: Install Client Dependencies
        working-directory: ./client
        run: npm ci

      - name: Build Client
        working-directory: ./client
        env:
          NODE_OPTIONS: --max_old_space_size=4096
          CI: false
        run: npm run build

      # No need to copy build files; they are already in the expected location

      - name: Deploy to Heroku
        uses: akhileshns/heroku-deploy@v3.12.12
        with:
          heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
          heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
          heroku_email: ${{ secrets.HEROKU_EMAIL }}
          process_type: web worker
          usedocker: false

Express Server Code:

expressApp.use(express.static(path.join(__dirname, 'client/build')));

expressApp.get('/*', (req, res) => {
  res.sendFile(path.join(__dirname, 'client/build', 'index.html'));
});

If I delete in .gitignore the build folder I am able to see that I have a build folder in Heroku through its CLI, but I miss the node_modules. When I navigate to my url it shows the index.html is loaded and I can see the bundle but nothing renders.

When I delete node_modules from .gitignore I get the following error during Heroku Deployment after build and installations succeeded:

Error: Error: spawnSync /bin/sh ENOBUFS

So what I did was to add the dependencies through Heroku and not Github Actions:

"heroku-postbuild": "cd client && npm install",

Now I can see I do have everything but still nothing renders:

Running bash on ⬢ your-heroku-app... up, run.7224
~ $ ls
Procfile  README.md  Server.js  client  node_modules  package-lock.json  package.json  src  worker.js
~ $ cd client/
~/client $ ls
README.md  build  node_modules  package-lock.json  package.json  public  src  tailwind.config.js

The issue you’re facing, where the front-end isn’t rendering even though the index.html and bundle are served, could be related to several factors in your setup. Let’s break it down and troubleshoot step-by-step based on the folder structure, GitHub Actions setup, and Heroku deployment you’ve provided.

1. Check React Production Build Path:

Your Express server is set up to serve static files from client/build, but if nothing renders on the front-end, there might be issues in the build process, or the paths might not be correct. Ensure that the build files are correctly generated and accessible.

Verify the following in your Express server:

const express = require('express');
const path = require('path');
const expressApp = express();

// Serve static files from the React app build directory
expressApp.use(express.static(path.join(__dirname, 'client/build')));

// Serve React's index.html for any unknown routes (single-page app setup)
expressApp.get('/*', (req, res) => {
  res.sendFile(path.join(__dirname, 'client/build', 'index.html'));
});

const PORT = process.env.PORT || 5000;
expressApp.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
  • Verify the static file serving: After deployment, you can use the Heroku CLI to check if the client/build folder contains all the expected files (e.g., index.html, bundle.js, etc.).
heroku run bash -a your-heroku-app
cd client/build
ls

2. GitHub Actions – Build Process:

It seems you’re building the client app on GitHub Actions and deploying it to Heroku. This should work fine, but ensure that the build folder is correctly deployed to Heroku and the environment is set to production.

In your workflow:

  • The build command should generate the client/build folder.
  • Make sure you have set the environment to production:
- name: Build Client
  working-directory: ./client
  env:
    NODE_ENV: production
    NODE_OPTIONS: --max_old_space_size=4096
    CI: false
  run: npm run build

Important: If the build folder is correctly generated during the GitHub Actions build process, ensure that you’re not overwriting it with any post-build steps on Heroku.

3. Heroku Post-Build Setup:

You’re running heroku-postbuild to reinstall node_modules inside the client folder. However, running npm install in heroku-postbuild could overwrite or interfere with your GitHub Actions build if it isn’t necessary.

  • Ideally, the heroku-postbuild script should only handle tasks that are needed on Heroku (not rebuilding or reinstalling everything).
  • Check if removing the heroku-postbuild step resolves the issue since GitHub Actions should have already built the client app and deployed the build folder to Heroku.

Remove heroku-postbuild if unnecessary:

"scripts": {
  "heroku-postbuild": "cd client && npm install" // Try removing this
}

If you want to keep it, make sure to just reinstall server-side dependencies if needed, but don’t rebuild the client here. For instance:

"scripts": {
  "heroku-postbuild": "npm install"
}

4. Heroku Environment Variables:

Make sure that Heroku has the correct environment variables for your app, especially NODE_ENV=production. You can set them via the Heroku CLI:

heroku config:set NODE_ENV=production

5. Troubleshooting Rendering Issues:

  • Inspect Network Requests: Use your browser’s developer tools (F12) to inspect the network tab and see if any JS, CSS, or other assets are failing to load. If there are 404 errors, it could be due to incorrect paths.
  • Check Console Errors: Open the browser console to check for any JavaScript errors that might indicate why the app is not rendering.

6. Memory Errors (ENOBUFS):

The error spawnSync /bin/sh ENOBUFS indicates that you’re running out of memory or buffer space during the deployment process. This is common when trying to manage large files or processes with limited memory.

  • Use GitHub Actions for building: Since you switched to GitHub Actions due to memory limits on Heroku, ensure that the build process is fully handled by GitHub, and you’re not trying to rebuild everything on Heroku.
  • Check Heroku Logs: Use the following command to monitor logs during the deployment process and see if there are memory spikes:
heroku logs --tail

7. Double-Check GitHub Actions Deployment:

Make sure that the GitHub Actions deployment is correctly configured. If the build files aren’t being deployed properly, add a verification step to check that the build folder exists before deploying to Heroku:

- name: Verify Client Build
  run: ls ./client/build

Summary:

  • Ensure the React build folder is correctly deployed and served from Heroku.
  • Consider removing unnecessary heroku-postbuild steps.
  • Verify that NODE_ENV=production is set for both the GitHub Actions build and Heroku environment.
  • Check for errors in the browser’s console and network tabs.
  • Monitor memory usage on Heroku using the heroku logs --tail command.

Try these steps and see if it resolves the issue! Let me know if the problem persists.