diff --git a/README.md b/README.md index 8dc53f02..8dfe4904 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # Reaction Commerce Buildpack +This buildpack is a fork of [meteor-buildpack-horse](https://github.com/swrdfish/meteor-buildpack-horse.git) with our +modifications to ease deployment and overcome common failures (like boot timeout). + To use this with heroku: 1. Set up your app to [deploy to heroku with git](https://devcenter.heroku.com/articles/git). 2. Set this repository as the buildpack URL: - heroku buildpacks:set https://github.com/swrdfish/meteor-buildpack-horse.git + heroku buildpacks:set https://github.com/Zanobo/reaction-buildpack.git 3. Add the MongoLab addon: - + heroku addons:create mongolab 4. Set the `ROOT_URL` environment variable. This is required for bundling and running the app. Either define it explicitly, or enable the [Dyno Metadata](https://devcenter.heroku.com/articles/dyno-metadata) labs addon to default to `https://.herokuapp.com`. @@ -31,6 +34,20 @@ The following are some important environment variables for bundling and running - `BUILDPACK_CLEAR_CACHE`: This buildpack stores the meteor installation in the [CACHE_DIR](https://devcenter.heroku.com/articles/buildpack-api#caching) to speed up subsequent builds. Set `BUILDPACK_CLEAR_CACHE=1` to clear this cache on startup. - `BUILD_OPTIONS`: Set to any additional options you'd like to add to the invocation of `meteor build`, for example `--debug` or `--allow-incompatible-update`. +### Boot proxy + +Sometimes reaction takes too much time to start and Heroku thinks that there's a problem with your app. To sidestep this +we have a simple proxy that answers every request until it's ready. + +It accepts the following environment variables: + + - `USE_BOOT_PROXY`: Set `USE_BOOT_PROXY=1` to enable it. It's disabled by default. + - `PING_PATH`: The route we use to test if your app is ready. Defaults to `/`. You need to add a leading slash. + - `PING_INTERVAL`: Interval between probes, in seconds. Defaults to 1 second. + - `BOOT_TIMEOUT`: Maximum time to wait for your app in seconds. Defaults to 3600 (1 Hour). If reached we exit with an + error. + - `BOOTING_URL`: Proxies transparently to this url while your app is booting if provided. + ## Extras The basic buildpack should function correctly for any normal-ish meteor app, @@ -52,7 +69,3 @@ subdirectories. Those directories are added to `$PATH` and So `$COMPILE_DIR/bin` etc are great places to put any extra binaries or stuff if you need to in custom extras. - -## Tips & Tricks - -Please help us add tips and tricks to the [wiki](https://github.com/AdmitHub/meteor-buildpack-horse/wiki) for further help, like usage with Dokku or other environments. \ No newline at end of file diff --git a/bin/boot_proxy.js b/bin/boot_proxy.js new file mode 100755 index 00000000..35fe7a49 --- /dev/null +++ b/bin/boot_proxy.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +var spawn = require('child_process').spawn; +var http = require('http'); +var httpProxy = require('http-proxy'); + + +var USE_BOOT_PROXY = (['1', 'true', 'yes', 1].indexOf((process.env.USE_BOOT_PROXY || '').toLowerCase()) !== -1); + +var PORT = process.env.PORT || 3000; +var SUBPROCESS_PORT = parseInt(process.env.SUBPROCESS_PORT) || 3030; +var PING_PATH = process.env.PING_PATH || '/'; +var PING_INTERVAL = parseInt(process.env.PING_PATH) || 1; +var BOOT_TIMEOUT = parseInt(process.env.BOOT_TIMEOUT) || 60 * 60; +var BOOTING_URL = process.env.BOOTING_URL; + + +var ROOT_URL = process.env.ROOT_URL; +var HEROKU_APP_NAME = process.env.HEROKU_APP_NAME; + +if (!ROOT_URL && HEROKU_APP_NAME) { + ROOT_URL = 'https://' + HEROKU_APP_NAME + '.herokuapp.com'; +} else { + ROOT_URL = 'http://localhost'; +} + + +var child; +function start_subprocess() { + var command = process.argv.splice(2).join(' '); + var env = Object.assign({}, process.env); + + env.ROOT_URL = ROOT_URL; + if (USE_BOOT_PROXY) { + env.PORT = SUBPROCESS_PORT; + } + + child = spawn(command, { + stdio: 'inherit', + shell: true, + env: env, + }); + + + child.on('close', function (code) { + process.exit(code); + }); + + child.on('error', function (err) { + console.error(`Failed to run: ${command}`); + console.error(err); + process.exit(127); + }); + + + return child; +} + + + +start_subprocess(); + +if (USE_BOOT_PROXY) { + var booted = false; + + var pinger = setInterval(function () { + var options = { + port: SUBPROCESS_PORT, + method: 'GET', + path: PING_PATH, + timeout: (1000*PING_INTERVAL) / 2, + }; + + var req = http.request(options, function (res) { + if (res.statusCode !== 200) { + return; + } else { + console.log('Application booted'); + clearInterval(pinger); + booted = true; + } + }); + + req.on('error', function (e) { + // Silence is golden... + // console.error(`problem with request: ${e.message}`); + }); + req.end(); + }, 1000*PING_INTERVAL); + + + + var bootingProxy; + var proxy = new httpProxy.createProxyServer({ + target: { + port: SUBPROCESS_PORT, + }, + ws: true, + }); + + if (BOOTING_URL) { + bootingProxy = new httpProxy.createProxyServer({ + target: BOOTING_URL, + changeOrigin: true, + ws: true, + }); + + bootingProxy.on('error', function (err, req, res) { + res.writeHead(500, { + 'Content-Type': 'text/plain' + }); + + console.error(err); + res.end('There was an error'); + }); + } + + var proxyServer = http.createServer(function (req, res) { + if (booted) { + proxy.web(req, res); + } else { + if (bootingProxy) { + bootingProxy.web(req, res); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Waiting for app to boot...'); + } + } + }); + + proxyServer.on('upgrade', function (req, socket, head) { + if (booted) { + proxy.ws(req, socket, head); + } else { + if (bootingProxy) { + bootingProxy.ws(req, socket, head); + } + } + }); + + proxy.on('error', function (err, req, res) { + res.writeHead(500, { + 'Content-Type': 'text/plain' + }); + + console.error(err); + res.end('There was an error'); + }); + + proxyServer.listen(PORT); + + setTimeout(function () { + if (!booted) { + console.error('Application not booted after ' + BOOT_TIMEOUT + ' seconds. Quitting...'); + child.kill(); + process.exit(64); + } + }, BOOT_TIMEOUT*1000); +} + + +// Dummy interval to keep main loop busy so we don't exit early. +setInterval(function () { }, 1000) diff --git a/bin/compile b/bin/compile index 4e1e7b3f..7a39c384 100755 --- a/bin/compile +++ b/bin/compile @@ -167,11 +167,10 @@ fi # assets at runtime, and thus they are not available for bundling unless meteor # has been launched. To opt-in to this, set BUILDPACK_PRELAUNCH_METEOR=1. if [ -n "${BUILDPACK_PRELAUNCH_METEOR+1}" ]; then - echo "-----> BUILDPACK_PRELAUNCH_METEOR: Pre-launching meteor to build packages assets" + echo "-----> BUILDPACK_PRELAUNCH_METEOR: Removing android" # Remove the Android platform because it fails due to the Android tools not # being installed, but leave the iOS platform because it's ignored. HOME=$METEOR_DIR $METEOR remove-platform android || true - HOME=$METEOR_DIR timeout -s9 60 $METEOR --settings settings.json || true fi @@ -182,6 +181,18 @@ fi $METEOR_NPM install reaction-cli node_modules/.bin/reaction plugins load +if [ -n "${BUILDPACK_PRELAUNCH_METEOR+1}" ]; then + echo "-----> BUILDPACK_PRELAUNCH_METEOR: Pre-launching reaction to build packages assets" + HOME=$METEOR_DIR timeout -s9 600 node_modules/.bin/reaction run || true +fi + + +# +# Install our boot proxy +# + +$METEOR_NPM install --save http-proxy +cp "$BUILDPACK_DIR/bin/boot_proxy.js" "$COMPILE_DIR/bin" # Now on to bundling. Don't put the bundle in $APP_CHECKOUT_DIR during # bundling, or it will recurse, trying to bundle up its own bundling. diff --git a/bin/release b/bin/release index c3fed2b0..a1ecab7c 100755 --- a/bin/release +++ b/bin/release @@ -9,5 +9,5 @@ cat <