Skip to content

Heroku Build Configuration and Package.json

David edited this page Jul 8, 2022 · 5 revisions

Background: 2022Jul07 - I (@ddfridley) have been having a lot of trouble building on heroku over the past week or 2. I've been doing a lot to debug it, and make it work. While I have it working, I have not been able to prove that I found the root cause, though I have done several things to make it better. I am capturing what I have done, and why for better understanding. This project, undebate-ssp depends on undebate, civil-server and civil-client. The undebate projects (which depends on civil-server) builds separately on heroku okay, and the civil-server, which depends on civil-client also builds separately on heroku.

Be aware of the 15 Minute build limit on Heroku

There is a 15 minute limit to builds on Heroku

Why

The problem I've been having is that builds never finish. Often they hang for days. But the key is that they don't complete in 15 minutes. It seems like 15 minutes should be a problem, but with this layers repo architecture where undebate-ssp includes undebate and civil-client and civil-server the build time accumulates.

optionalDependencies

Move storybook, jest, eslint, and other 'test' dependencies to optionalDependencies in package.json.

    "optionalDependencies": {
        "@jest/globals": "^27.4.6",
        "@shelf/jest-mongodb": "^2.2.0",
        "@storybook/addon-actions": "^6.4.9",
        "@storybook/addon-essentials": "^6.4.9",
        "@storybook/addon-interactions": "^6.4.19",
        "@storybook/addon-links": "^6.4.9",
        "@storybook/builder-webpack5": "^6.4.9",
        "@storybook/jest": "^0.0.10",
        "@storybook/manager-webpack5": "^6.4.9",
        "@storybook/react": "^6.4.9",
        "@storybook/testing-library": "^0.0.9",
        "concurrently": "^6.4.0",
        "enzyme": "^3.11.0",
        "enzyme-adapter-react-16": "^1.15.6",
        "eslint": "^8.4.1",
        "eslint-config-airbnb": "^19.0.2",
        "eslint-config-prettier": "^8.3.0",
        "eslint-plugin-import": "^2.25.3",
        "eslint-plugin-jsx-a11y": "^6.5.1",
        "eslint-plugin-prettier": "^4.0.0",
        "eslint-plugin-react": "^7.27.1",
        "eslint-plugin-react-hooks": "^4.3.0",
        "husky": "^4.3.8",
        "jest": "^27.4.7",
        "jest-enzyme": "^7.1.2",
        "nodemon": "^2.0.15",
        "prettier": "^2.5.1",
        "pretty-quick": "^3.1.2",
        "socket.io": "^4.4.1",
        "socket.io-client": "^4.4.1",
        "webpack-dev-server": "^4.6.0"
    },

Set the NPM_CONFIG_OMIT env variable to omit the optional dependencies.

heroku config:set NPM_CONFIG_OMIT=optional

Why

Npm on heroku will not waste time and memory fetching and building them. Slug size went from 130M to 81M.

Using prepare script rather than postinstall

In all the repos I moved the postinstall.sh code into build.sh and ran that under the prepare script rather than postinstall.

Why

For production, you may set NODE_ENV=production which will cause heroku to remove the devDependencies after the build. That is it removes them after the prepare script, and before the postinstall script. So they won't be there when needed.

NPM_PROJECT Env Variable

The build.sh scripts now check for an NPM_PROJECT environment variable and they don't run webpack if the script isn't for the project being built.

if test \"$NPM_PROJECT\" = \"\" || test \"$NPM_PROJECT\" == \"undebate-ssp\" ; then {
  npm run packbuild  || {
    echo Could not webpack;
    exit 1
  }
}; fi

Why

The undebate repo is a dependency and being built when loaded into undebate-ssp project, undebate's main.js does not need to be generated. Webpack consumes a lot of memory when it runs and can lead to out of memory errors. So, instead just don't build it if it's not necessary.

Beware of bcrypt

bcrypt is a package that wants to compile and install C code on node in order to provide optimal password encryption. Sometimes new versions of node don't work with bcrypt.

Why

Some of the builds that failed were on [email protected] and then things that were working were on [email protected] just a few days apart. But when I set the node engine back to 16.15.1 after all of the other changed described here, the build succeeded.

Set engines in package.json

    "engines": {
        "npm": "8.13.2",
        "node": "16.16.0"
    }

Why

The node and npm you run locally won't change very often - unless you change it. But the node and npm that get used on heroku will change depending on what's the latest available, and what Heroku stack you are using (eg heroku-22). Also see why for bcrypt.

Set USE_NPM_INSTALL=true on Heroku

Heroku has started using npm ci instead of npm install when you push your repo. Npm ci will erase your node_modules directory and start anew every time you push a commit. This will take longer, and if you are running into problems with your build taking to long, then if you use npm install you can do it incrementally. The first push may fail, but if you push it again, then it may work.

Webpack devtool: 'inline-cheap-source-map'

webpack-prod.config.js

module.exports = {
    mode: 'production',
    context: path.resolve(__dirname, 'dist'), // dist because app failed when building this as a node_module in another component
    entry: {
        main: './client/main-app.js',
    },
    devtool: 'inline-cheap-source-map', // cheap so it won't run out of memory on heroku
    ...

In the production build for webpack, use this devtool to reduce out of memory errors.

Why

To prevent out of memory errors

Use peerDependencies

Because undebate-ssp depends on undebate, which depends on civil-server and civil-client I put them on in peerDependencies

    "peerDependencies": {
        "civil-client": "github:EnCiv/civil-client#heroku-oom",
        "civil-server": "github:EnCiv/civil-server#heroku-oom",
        "undebate": "github:EnCiv/undebate#heroku-oom"
    }

If undebate-ssp depends on one version of civil-server, while undebate depends on a different one, then civil-server may get fetched and built twice. This is especially a concern since these things are changing frequently. So they are in peerDependencies in all repos in order to minimize extra fetches and builds.