Sharing: a rudimentary way to set up a holding page while upgrading

Note: not sure where to put this, feel free to move this to a more appropriate place if I missed something.

Just wanted to share a quick hacky way I’ve devised in the past week to put up a quick page when doing a Discourse upgrade. I did read all the existing guides out there, but I know nothing about nginx and a dual-container setup looked very intimidating.

tl;dr; I made something, the code is below. :backhand_index_pointing_down:

Node.js to the rescue

So I thought: what is the quickest way to just spin up a super tiny web server with a single page? Then I remembered node.js was installed anyway, could I maybe get away with just using that?

A quick search and copy/paste from Stackoverflow later I had a tiny thing listening to port 80!

The process is super manual, but I think it’s okay, I need to monitor the logging when the upgrade is running anyway, so I wait to see the signal:

Stopping old container
+ /usr/bin/docker stop -t 600 app

At that point, I’d have another SSH session open where I quickly run:

touch index.html
node server.js 80

I touch the index.html file so I can read the update date from it, and replace a placeholder. In my index.html I have this line:

<p>This page was last updated at: LASTUPDATE</p>

In the server.js file I replace that LASTUPDATE with the date/time when the index.html file was last updated. This just gives people an idea of when the upgrade started and I do note in the page it will take 10-15 minutes to complete.

This had me easily running something on my dev server as a test and I felt ready to do this on live.

Oops, we need https :sweat_smile:

Time to upgrade the live server and show people my new quick holding page!

That didn’t go as expected. The live site is behind Cloudflare and requires full end-to-end certificates to be available so I took down the forum for 15 minutes and only a Cloudflare error page to show for it :person_facepalming:

Luckily, some more copy/pasting from Stack Overflow helped. I switched the server from being http to https and got a cert.key and cert.pem files to make sure the certificate chain was intact.

The next upgrade was a bigger success and the holding pages showed up as designed:

Just show me the code!

Here’s the two files I’m using for this holding page:

index.html

<!DOCTYPE html>
<html lang="en-US">
<head>
        <meta charset="utf-8">
        <title>We'll be right back</title>
</head>
<body>
        <h1>We'll be right back</h1>
        <p>Sorry for the temporary outage, we're currently applying an update, this usually take 10-15 minutes.</p>
        <p>This page was last updated at: LASTUPDATE</p>
</body>
</html>

server.js

var http = require("http"),
  https = require("https"),
  url = require("url"),
  path = require("path"),
  fs = require("fs"),
  port = process.argv[2] || 81;

const options = {
  key: fs.readFileSync("cert.key").toString(),
  cert: fs.readFileSync("cert.pem").toString(),
};

https
  .createServer(options, function (request, response) {
    var uri = url.parse(request.url).pathname,
      filename = path.join(process.cwd(), uri);

    fs.exists(filename, function (exists) {
      if (!exists) {
        response.writeHead(404, { "Content-Type": "text/plain" });
        response.write("404 Not Found\n");
        response.end();
        return;
      }

      let indexFile = (filename += "index.html");
      if (fs.statSync(filename).isDirectory()) indexFile;

      fs.readFile(filename, "utf8", function (err, file) {
        if (err) {
          response.writeHead(500, { "Content-Type": "text/plain" });
          response.write(err + "\n");
          response.end();
          return;
        }

        var stats = fs.statSync(indexFile);
        var mtime = stats.mtime;

        response.writeHead(200);
        file = file.replace(/LASTUPDATE/g, mtime);
        response.write(file, "utf8");
        response.end();
      });
    });
  })
  .listen(parseInt(port, 10));

console.log(
  "Static file server running at\n  => https://localhost:" +
    port +
    "/\nCTRL + C to shutdown"
);

As noted above, you can run it as follows:

touch index.html
node server.js 443

The 443 argument is the port you want to run it on, by default it runs on port 81 so that I can do a curl to test what the output will be, before I actually do this during the upgrade:

curl -k https://localhost:81

At some point during the Discourse upgrade, when it completes, you will see:

Error starting userland proxy: listen tcp4 0.0.0.0:443: bind: address already in use.

That’s when I terminate the Node server and start Discourse again:

./launcher restart app

And that’s it. Hope this helps someone!

3 Likes