diff --git a/keyserver/flow-typed/npm/stoppable_vx.x.x.js b/keyserver/flow-typed/npm/stoppable_vx.x.x.js
new file mode 100644
--- /dev/null
+++ b/keyserver/flow-typed/npm/stoppable_vx.x.x.js
@@ -0,0 +1,10 @@
+// flow-typed signature: 4ffffaa246e31defe6bd7054e7869482
+// flow-typed version: <<STUB>>/stoppable_v1.1.0/flow_v0.202.1
+
+declare module 'stoppable' {
+  declare module.exports: {
+    <T: http$Server | ?https$Server>(server: T, grace?: number): T & {
+      +stop: (callback?: (error: ?Error, gracefully: boolean) => void) => void,
+    }
+  }
+}
diff --git a/keyserver/package.json b/keyserver/package.json
--- a/keyserver/package.json
+++ b/keyserver/package.json
@@ -85,6 +85,7 @@
     "sharp": "^0.30.5",
     "siwe": "^2.1.4",
     "sql-template-strings": "^2.2.2",
+    "stoppable": "^1.1.0",
     "tcomb": "^3.2.29",
     "twin-bcrypt": "^2.1.1",
     "uuid": "^3.4.0",
diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js
--- a/keyserver/src/keyserver.js
+++ b/keyserver/src/keyserver.js
@@ -11,6 +11,7 @@
 import expressWs from 'express-ws';
 import os from 'os';
 import qrcode from 'qrcode';
+import stoppable from 'stoppable';
 
 import './cron/cron.js';
 import { qrCodeLinkURL } from 'lib/facts/links.js';
@@ -110,6 +111,23 @@
 
   if (cluster.isMaster) {
     if (isPrimaryNode) {
+      const healthCheckApp = express();
+      healthCheckApp.use(express.json({ limit: '250mb' }));
+      healthCheckApp.get('/health', (req: $Request, res: $Response) => {
+        res.send('OK');
+      });
+
+      // We use stoppable to allow forcibly stopping the health check server
+      // on the master process so that non-master processes can successfully
+      // initialize their express servers on the same port without conflict
+      const healthCheckServer = stoppable(
+        healthCheckApp.listen(
+          parseInt(process.env.PORT, 10) || 3000,
+          listenAddress,
+        ),
+        0,
+      );
+
       const didMigrationsSucceed: boolean = await migrate();
       if (!didMigrationsSucceed) {
         // The following line uses exit code 2 to ensure nodemon exits
@@ -117,6 +135,18 @@
         // in https://github.com/remy/nodemon/issues/751
         process.exit(2);
       }
+
+      if (healthCheckServer) {
+        await new Promise((resolve, reject) => {
+          healthCheckServer.stop(err => {
+            if (err) {
+              reject(err);
+            } else {
+              resolve();
+            }
+          });
+        });
+      }
     }
 
     if (shouldDisplayQRCodeInTerminal && isPrimaryNode) {
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -22492,6 +22492,11 @@
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
   integrity sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==
 
+stoppable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
+  integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
+
 stopwords-iso@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/stopwords-iso/-/stopwords-iso-1.1.0.tgz#dc303db6b0842d4290bc1339b4eaf37b94219395"