Summary of the issue
I've recently tried to deploy my local application to Heroku. It's built with a Flask backend and a React/Redux frontend. After working through the intricacies of Heroku (procfiles, where it reads package.json, etc.) I'm able to get the backend to show (example: the flask-admin section is working as well as my database), but I'm still unable to reach the frontend (react) portion of my site. There are no errors that I can spot in the Heroku logs and on local version my application works perfectly fine when I start up my python server and do NPM start in the static directory.
Any idea why the front end wouldn't be showing or how to access it?
Logs:
I've removed some sensitive information from the details, but here's what heroku logs --tail gives me when I try to refresh the app.
2018-02-11T01:18:01.000000+00:00 app[api]: Build succeeded
2018-02-11T01:21:12.305017+00:00 heroku[web.1]: Starting process with command `gunicorn main:app`
2018-02-11T01:21:16.374150+00:00 heroku[web.1]: State changed from starting to up
2018-02-11T01:21:15.948707+00:00 app[web.1]: [2018-02-11 01:21:15 +0000] [4] [INFO] Starting gunicorn 19.6.0
2018-02-11T01:21:15.949430+00:00 app[web.1]: [2018-02-11 01:21:15 +0000] [4] [INFO] Listening at: http://0.0.0.0:29162
2018-02-11T01:21:19.278810+00:00 heroku[router]: at=info method=GET path="/" host=removed.herokuapp.com request_id=bd74ea4c-3e3a-403b-8850-198b7dec20e2 fwd="104.152.1.62" dyno=web.1 connect=1ms service=2146ms status=200 bytes=2895 protocol=https
2018-02-11T01:21:19.650759+00:00 heroku[router]: at=info method=GET path="/dist/bundle.css" host=removed.herokuapp.com request_id=48183249-fb12-4c7a-9a53-2a57ab58d89b fwd="104.152.1.62" dyno=web.1 connect=0ms service=3ms status=200 bytes=2895 protocol=https
2018-02-11T01:21:19.816113+00:00 heroku[router]: at=info method=GET path="/dist/bundle.js" host=removed.herokuapp.com request_id=1c8b258b-4187-4df6-af35-784e62fb97e5 fwd="104.152.1.62" dyno=web.1 connect=1ms service=3ms status=200 bytes=2895 protocol=https
When I view source I see the code from my index.html file (in the static folder, which is correct) and this particular line is highlighted red which makes me think it's missing bundle.js?
<script src="/dist/bundle.js"></script>
When I try to visit the front end, none of it is showing. I'm really unsure where to look next.
Initial ideas:
Is my server.js file actually starting on Heroku? I have a "start" script in my package.json, but if procfile is executing Python...does it actually start? How do I make it start without putting it into a postinstall or post-build step?
Is webpack messing something up. My production webpack is slightly different from my development so maybe during build:production it's getting messed up? However, that doesn't explain why build is always succeeding.
Is my server.js or webpack forcing formatting on the index.html file and so it can't parse properly? Perhaps this is why I'm seeing
SyntaxError: expected expression, got '< bundle.js:1in the console?
Update: I received a note from the Heroku support team letting me know that Heroku dynos are not built to support multi processes applications like mine. As a result, they feel like I'd need to make some signficant changes to have all the HTTP requests forwarding through one dyno. Perhaps this is my issue?
Here are files that may help debug:
Server.js
const http = require('http');
const express = require('express');
const httpProxy = require('http-proxy');
const path = require('path');
const apiPort = process.env.PORT || 8081;
const proxy = httpProxy.createProxyServer({});
const app = express();
app.use(require('morgan')('short'));
(function initWebpack() {
const webpack = require('webpack');
const webpackConfig = require('./webpack/common.config');
const compiler = webpack(webpackConfig);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true, publicPath: webpackConfig.output.publicPath,
}));
app.use(require('webpack-hot-middleware')(compiler, {
log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000,
}));
app.use(express.static(path.join(__dirname, '/')));
}());
app.all(/^\/api\/(.*)/, (req, res) => {
proxy.web(req, res, { target: `http://0.0.0.0:${apiPort}` });
});
app.get(/.*/, (req, res) => {
res.sendFile(path.join(__dirname, '/index.html'));
});
const server = http.createServer(app);
server.listen(process.env.PORT || 8080, () => {
const address = server.address();
console.log('Listening on: %j', address);
console.log(' -> that probably means: http://0.0.0.0:%d', address.port);
});
File Structure
ROOT
├──/application
│ ├── models.py
│ ├── app.py
├──/static
│ ├──/bin
│ ├──/dist
│ │ ├──bundle.js
│ ├──/node_modules
│ ├──/src
│ │ ├──/actions
│ │ ├──/components
│ │ │ ├──/examplecomponenthere
│ │ │ │ ├──index.js (for example component)
│ │ ├──/constants
│ │ ├──/containers
│ │ ├──/reducers
│ │ ├──/store
│ │ ├──/webpack
│ ├──index.html
│ ├──package.json (the true one)
│ ├──server.js
├──/tests
├──config.py
├──index.py
├──main.py
├──package.json (one to help heroku start)
├──procfile
├──requirements.txt.
├──setup.py
├──tests.py
Package.json in root This file exists because I'm running a multi build. Heroku doesn't seem to recognize the package.json in static until I use this one to push it over there.
{
"name": "rmmd",
"version": "0.0.1",
"engines": {
"node": "6.11.1",
"npm": "3.10.10"
},
"scripts": {
"start": "node static/bin/server.js",
"heroku-postbuild": "cd static && npm install && npm run build:production"
}
}
Package.json in static
{
"name": "redux-easy-boilerplate",
"version": "1.3.3",
"description": "",
"scripts": {
"clean": "rimraf dist",
"build": "webpack --progress --verbose --colors --display-error-details --config webpack/common.config.js",
"build:production": "npm run clean && npm run build",
"lint": "eslint src",
"start": "node bin/server.js",
"test": "karma start"
},
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"react",
"reactjs",
"boilerplate",
"redux",
"hot",
"reload",
"hmr",
"live",
"edit",
"webpack"
],
"author": "https://github.com/anorudes, https://github.com/keske",
"license": "MIT",
"devDependencies": {
"webpack-dev-middleware": "^1.5.0",
"webpack-dev-server": "^1.14.1",
"webpack-hot-middleware": "^2.6.0",
},
"dependencies": {
"ant-design-pro": "^0.3.1",
"antd": "^3.0.0",
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"react-bootstrap": "^0.31.0",
"redux-devtools-extension": "^2.13.2",
"autoprefixer": "6.5.3",
"axios": "^0.15.3",
"babel-core": "^6.4.5",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.1",
"babel-plugin-import": "^1.2.1",
"babel-plugin-react-transform": "^2.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-polyfill": "^6.3.14",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-react-hmre": "^1.0.1",
"babel-preset-stage-0": "^6.3.13",
"bootstrap": "^3.3.5",
"bootstrap-loader": "^1.2.0-beta.1",
"bootstrap-sass": "^3.3.6",
"bootstrap-webpack": "0.0.5",
"classnames": "^2.2.3",
"css-loader": "^0.26.4",
"csswring": "^5.1.0",
"deep-equal": "^1.0.1",
"eslint": "^3.4.0",
"eslint-config-airbnb": "13.0.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^3.0.1",
"eslint-plugin-react": "^6.1.2",
"expect": "^1.13.4",
"exports-loader": "^0.6.2",
"expose-loader": "^0.7.1",
"express": "^4.13.4",
"express-open-in-editor": "^1.1.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"gapi": "0.0.3",
"history": "^4.4.1",
"http-proxy": "^1.12.0",
"imports-loader": "^0.6.5",
"jasmine-core": "^2.4.1",
"jquery": "^3.1.0",
"jwt-decode": "^2.1.0",
"karma": "^1.2.0",
"karma-chrome-launcher": "^2.0.0",
"karma-mocha": "^1.1.1",
"karma-webpack": "^1.7.0",
"less": "^2.7.2",
"less-loader": "^2.2.3",
"lodash": "^4.5.1",
"material-ui": "^0.16.4",
"mocha": "^3.0.2",
"morgan": "^1.6.1",
"node-sass": "^3.4.2",
"postcss-import": "^9.0.0",
"postcss-loader": "^1.1.1",
"q": "^1.4.1",
"qs": "^6.1.0",
"rc-datepicker": "^4.0.1",
"react": "^15.3.1",
"react-addons-css-transition-group": "^15.3.1",
"react-bootstrap": "^0.31.0",
"react-calendar-component": "^1.0.0",
"react-date-picker": "^5.3.28",
"react-datepicker": "^0.37.0",
"react-document-meta": "^2.0.0-rc2",
"react-dom": "^15.1.0",
"react-forms": "^2.0.0-beta33",
"react-hot-loader": "^1.3.0",
"react-loading-order-with-animation": "^1.0.0",
"react-onclickoutside": "^5.3.3",
"react-redux": "^4.3.0",
"react-router": "3.0.0",
"react-router-redux": "^4.0.0",
"react-tap-event-plugin": "^2.0.1",
"react-transform-hmr": "^1.0.1",
"redux": "^3.2.1",
"redux-form": "^6.0.1",
"redux-logger": "2.7.4",
"redux-thunk": "^2.1.0",
"resolve-url-loader": "^1.4.3",
"rimraf": "^2.5.0",
"sass-loader": "^4.0.0",
"style-loader": "^0.13.0",
"url-loader": "^0.5.7",
"webpack": "^1.12.11",
"webpack-merge": "^1.0.2",
"yargs": "^6.5.0"
}
}
Webpack Prod
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
devtool: 'source-map',
entry: ['bootstrap-loader/extractStyles', './src/index'],
output: {
publicPath: '/dist/',
},
module: {
loaders: [
{
test: /\.scss$/,
loader: 'style!css!postcss-loader!sass',
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
},
__DEVELOPMENT__: false,
}),
new ExtractTextPlugin('bundle.css'),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
},
}),
],
};
Webpack common
const path = require('path');
const autoprefixer = require('autoprefixer');
const postcssImport = require('postcss-import');
const merge = require('webpack-merge');
const development = require('./dev.config');
const production = require('./prod.config');
require('babel-polyfill').default;
const TARGET = process.env.npm_lifecycle_event;
const PATHS = {
app: path.join(__dirname, '../src'),
build: path.join(__dirname, '../dist'),
};
process.env.BABEL_ENV = TARGET;
const common = {
entry: [
PATHS.app,
],
output: {
path: PATHS.build,
filename: 'bundle.js',
},
resolve: {
extensions: ['', '.jsx', '.js', '.json', '.scss'],
modulesDirectories: ['node_modules', PATHS.app],
},
module: {
loaders: [{
test: /bootstrap-sass\/assets\/javascripts\//,
loader: 'imports?jQuery=jquery',
}, {
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/font-woff',
}, {
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/font-woff2',
}, {
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/octet-stream',
}, {
test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=application/font-otf',
}, {
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file',
}, {
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
loader: 'url?limit=10000&mimetype=image/svg+xml',
}, {
test: /\.js$/,
loaders: ['babel-loader'],
exclude: /node_modules/,
}, {
test: /\.css$/,
include: /node_modules/,
loaders: ['style-loader', 'css-loader'],
}, {
test: /\.png$/,
loader: 'file?name=[name].[ext]',
}, {
test: /\.jpg$/,
loader: 'file?name=[name].[ext]',
}],
},
postcss: (webpack) => (
[
autoprefixer({
browsers: ['last 2 versions'],
})
]
),
};
if (TARGET === 'start' || !TARGET) {
module.exports = merge(development, common);
}
if (TARGET === 'build' || !TARGET) {
module.exports = merge(production, common);
}
ProcFile
web: gunicorn main:app
Buildpacks in Heroku
heroku buildpacks:set heroku/python
heroku buildpacks:add heroku/nodejs