diff --git a/lib/in-process-server-test-helpers.js b/lib/in-process-server-test-helpers.js
index 1b2f90bd1750225206c6e18dc79d7c2a4070c54a..e7d1a6070170214e031c406fb72920bf2b824bc2 100644
--- a/lib/in-process-server-test-helpers.js
+++ b/lib/in-process-server-test-helpers.js
@@ -1,69 +1,28 @@
-/**
- * Helpers to run a Shields server in process.
- *
- * Usage:
- * let server;
- * before('Start running the server', function () {
- *   this.timeout(5000);
- *   server = serverHelpers.start();
- * });
- * after('Shut down the server', function () { serverHelpers.stop(server); });
- */
-
 'use strict'
 
-const config = require('./test-config')
 const serverConfig = require('./server-config')
-
-let startCalled = false
-
-/**
- * Start the server.
- *
- * @param {Number} port number (optional)
- * @return {Object} The scoutcamp instance
- */
-function start() {
-  if (startCalled) {
-    throw Error(
-      'Because of the way Shields works, you can only use this ' +
-        'once per node process. Once you call stop(), the game is over.'
-    )
+const Server = require('./server')
+
+function createTestServer({ port }) {
+  const config = {
+    ...serverConfig,
+    bind: {
+      port,
+      address: 'localhost',
+    },
+    ssl: {
+      isSecure: false,
+    },
+    baseUri: `http://localhost:${port}`,
+    cors: {
+      allowedOrigin: [],
+    },
+    rateLimit: false,
   }
-  startCalled = true
 
-  // Modifying config is a bit dirty, but it works, and avoids making bigger
-  // changes to server.js.
-  serverConfig.bind = {
-    host: 'localhost',
-    port: config.port,
-  }
-  const server = require('../server')
-  return server
-}
-
-/**
- * Reset the server, to avoid or reduce side effects between tests.
- *
- * @param {Object} server instance
- */
-function reset(server) {
-  server.reset()
-}
-
-/**
- * Stop the server.
- *
- * @param {Object} server instance
- */
-async function stop(server) {
-  if (server) {
-    await server.stop()
-  }
+  return new Server(config)
 }
 
 module.exports = {
-  start,
-  reset,
-  stop,
+  createTestServer,
 }
diff --git a/lib/request-handler.spec.js b/lib/request-handler.spec.js
index cd269f77d7e39f6e017d71cd4b37e19662252e10..8b81871a7ca49223bf5de799361b04809411149d 100644
--- a/lib/request-handler.spec.js
+++ b/lib/request-handler.spec.js
@@ -2,9 +2,9 @@
 
 const { expect } = require('chai')
 const fetch = require('node-fetch')
-const config = require('./test-config')
-const Camp = require('camp')
+const portfinder = require('portfinder')
 const analytics = require('./analytics')
+const Camp = require('camp')
 const { makeBadgeData: getBadgeData } = require('./badge-data')
 const {
   handleRequest,
@@ -12,11 +12,9 @@ const {
   _requestCache,
 } = require('./request-handler')
 
-const baseUri = `http://127.0.0.1:${config.port}`
-
-async function performTwoRequests(first, second) {
-  expect((await fetch(`${baseUri}${first}`)).ok).to.be.true
-  expect((await fetch(`${baseUri}${second}`)).ok).to.be.true
+async function performTwoRequests(baseUrl, first, second) {
+  expect((await fetch(`${baseUrl}${first}`)).ok).to.be.true
+  expect((await fetch(`${baseUrl}${second}`)).ok).to.be.true
 }
 
 function fakeHandler(queryParams, match, sendBadge, request) {
@@ -29,9 +27,15 @@ function fakeHandler(queryParams, match, sendBadge, request) {
 describe('The request handler', function() {
   before(analytics.load)
 
+  let port, baseUrl
+  beforeEach(async function() {
+    port = await portfinder.getPortPromise()
+    baseUrl = `http://127.0.0.1:${port}`
+  })
+
   let camp
   beforeEach(function(done) {
-    camp = Camp.start({ port: config.port, hostname: '::' })
+    camp = Camp.start({ port, hostname: '::' })
     camp.on('listening', () => done())
   })
   afterEach(function(done) {
@@ -53,7 +57,7 @@ describe('The request handler', function() {
     })
 
     it('should return the expected response', async function() {
-      const res = await fetch(`${baseUri}/testing/123.json`)
+      const res = await fetch(`${baseUrl}/testing/123.json`)
       expect(res.ok).to.be.true
       expect(await res.json()).to.deep.equal({ name: 'testing', value: '123' })
     })
@@ -68,7 +72,7 @@ describe('The request handler', function() {
     })
 
     it('should return the expected response', async function() {
-      const res = await fetch(`${baseUri}/testing/123.json`)
+      const res = await fetch(`${baseUrl}/testing/123.json`)
       expect(res.ok).to.be.true
       expect(await res.json()).to.deep.equal({ name: 'testing', value: '123' })
     })
@@ -100,12 +104,17 @@ describe('The request handler', function() {
         })
 
         it('should cache identical requests', async function() {
-          await performTwoRequests('/testing/123.svg', '/testing/123.svg')
+          await performTwoRequests(
+            baseUrl,
+            '/testing/123.svg',
+            '/testing/123.svg'
+          )
           expect(handlerCallCount).to.equal(1)
         })
 
         it('should differentiate known query parameters', async function() {
           await performTwoRequests(
+            baseUrl,
             '/testing/123.svg?label=foo',
             '/testing/123.svg?label=bar'
           )
@@ -114,6 +123,7 @@ describe('The request handler', function() {
 
         it('should ignore unknown query parameters', async function() {
           await performTwoRequests(
+            baseUrl,
             '/testing/123.svg?foo=1',
             '/testing/123.svg?foo=2'
           )
@@ -123,7 +133,7 @@ describe('The request handler', function() {
 
       it('should set the expires header to current time + defaultCacheLengthSeconds', async function() {
         register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 900 } })
-        const res = await fetch(`${baseUri}/testing/123.json`)
+        const res = await fetch(`${baseUrl}/testing/123.json`)
         const expectedExpiry = new Date(
           +new Date(res.headers.get('date')) + 900000
         ).toGMTString()
@@ -133,7 +143,7 @@ describe('The request handler', function() {
 
       it('should set the expires header to current time + maxAge', async function() {
         register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
-        const res = await fetch(`${baseUri}/testing/123.json?maxAge=3600`)
+        const res = await fetch(`${baseUrl}/testing/123.json?maxAge=3600`)
         const expectedExpiry = new Date(
           +new Date(res.headers.get('date')) + 3600000
         ).toGMTString()
@@ -143,7 +153,7 @@ describe('The request handler', function() {
 
       it('should ignore maxAge if maxAge < defaultCacheLengthSeconds', async function() {
         register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 600 } })
-        const res = await fetch(`${baseUri}/testing/123.json?maxAge=300`)
+        const res = await fetch(`${baseUrl}/testing/123.json?maxAge=300`)
         const expectedExpiry = new Date(
           +new Date(res.headers.get('date')) + 600000
         ).toGMTString()
@@ -153,7 +163,7 @@ describe('The request handler', function() {
 
       it('should set Cache-Control: no-cache, no-store, must-revalidate if maxAge=0', async function() {
         register({ cacheHeaderConfig: { defaultCacheLengthSeconds: 0 } })
-        const res = await fetch(`${baseUri}/testing/123.json`)
+        const res = await fetch(`${baseUrl}/testing/123.json`)
         expect(res.headers.get('expires')).to.equal(res.headers.get('date'))
         expect(res.headers.get('cache-control')).to.equal(
           'no-cache, no-store, must-revalidate'
@@ -167,14 +177,14 @@ describe('The request handler', function() {
         const expectedCacheKey = '/testing/123.json?colorB=123&label=foo'
         it('should match expected and use canonical order - 1', async function() {
           const res = await fetch(
-            `${baseUri}/testing/123.json?colorB=123&label=foo`
+            `${baseUrl}/testing/123.json?colorB=123&label=foo`
           )
           expect(res.ok).to.be.true
           expect(_requestCache.cache).to.have.keys(expectedCacheKey)
         })
         it('should match expected and use canonical order - 2', async function() {
           const res = await fetch(
-            `${baseUri}/testing/123.json?label=foo&colorB=123`
+            `${baseUrl}/testing/123.json?label=foo&colorB=123`
           )
           expect(res.ok).to.be.true
           expect(_requestCache.cache).to.have.keys(expectedCacheKey)
@@ -200,6 +210,7 @@ describe('The request handler', function() {
 
       it('should differentiate them', async function() {
         await performTwoRequests(
+          baseUrl,
           '/testing/123.svg?foo=1',
           '/testing/123.svg?foo=2'
         )
diff --git a/lib/server.js b/lib/server.js
new file mode 100644
index 0000000000000000000000000000000000000000..f8436e307e368f712c3aa2b10e60bbd2098ca139
--- /dev/null
+++ b/lib/server.js
@@ -0,0 +1,236 @@
+'use strict'
+
+const path = require('path')
+const Joi = require('joi')
+const Camp = require('camp')
+const makeBadge = require('../gh-badges/lib/make-badge')
+const GithubConstellation = require('../services/github/github-constellation')
+const { loadServiceClasses } = require('../services')
+const analytics = require('./analytics')
+const { makeBadgeData } = require('./badge-data')
+const log = require('./log')
+const { staticBadgeUrl } = require('./make-badge-url')
+const suggest = require('./suggest')
+const sysMonitor = require('./sys/monitor')
+const PrometheusMetrics = require('./sys/prometheus-metrics')
+const { makeSend } = require('./result-sender')
+const { handleRequest, clearRequestCache } = require('./request-handler')
+const { clearRegularUpdateCache } = require('./regular-update')
+
+const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
+const requiredUrl = optionalUrl.required()
+
+const configSchema = Joi.object({
+  bind: {
+    port: Joi.number()
+      .port()
+      .required(),
+    address: Joi.alternatives()
+      .try(
+        Joi.string()
+          .ip()
+          .required(),
+        Joi.string()
+          .hostname()
+          .required()
+      )
+      .required(),
+  },
+  metrics: {
+    prometheus: {
+      enabled: Joi.boolean().required(),
+      allowedIps: Joi.string(),
+    },
+  },
+  ssl: {
+    isSecure: Joi.boolean().required(),
+    key: Joi.string(),
+    cert: Joi.string(),
+  },
+  baseUri: requiredUrl,
+  redirectUri: optionalUrl,
+  cors: {
+    allowedOrigin: Joi.array()
+      .items(optionalUrl)
+      .required(),
+  },
+  persistence: {
+    dir: Joi.string().required(),
+    redisUrl: optionalUrl,
+  },
+  services: {
+    github: {
+      baseUri: requiredUrl,
+      debug: {
+        enabled: Joi.boolean().required(),
+        intervalSeconds: Joi.number()
+          .integer()
+          .min(1),
+      },
+    },
+    trace: Joi.boolean().required(),
+  },
+  profiling: {
+    makeBadge: Joi.boolean().required(),
+  },
+  cacheHeaders: {
+    defaultCacheLengthSeconds: Joi.number().integer(),
+  },
+  rateLimit: Joi.boolean().required(),
+  handleInternalErrors: Joi.boolean().required(),
+}).required()
+
+module.exports = class Server {
+  constructor(config) {
+    Joi.assert(config, configSchema)
+    this.config = config
+
+    this.githubConstellation = new GithubConstellation({
+      persistence: config.persistence,
+      service: config.services.github,
+    })
+    this.metrics = new PrometheusMetrics(config.metrics.prometheus)
+  }
+
+  get baseUrl() {
+    return this.config.baseUri
+  }
+
+  registerErrorHandlers() {
+    const { camp } = this
+
+    camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => {
+      const format = match[1]
+      const badgeData = makeBadgeData('404', query)
+      badgeData.text[1] = 'badge not found'
+      badgeData.colorscheme = 'red'
+      // Add format to badge data.
+      badgeData.format = format
+      const svg = makeBadge(badgeData)
+      makeSend(format, request.res, end)(svg)
+    })
+
+    camp.notfound(/.*/, (query, match, end, request) => {
+      end(null, { template: '404.html' })
+    })
+  }
+
+  registerServices() {
+    const { config, camp } = this
+    const { apiProvider: githubApiProvider } = this.githubConstellation
+
+    loadServiceClasses().forEach(serviceClass =>
+      serviceClass.register(
+        { camp, handleRequest, githubApiProvider },
+        {
+          handleInternalErrors: config.handleInternalErrors,
+          cacheHeaders: config.cacheHeaders,
+          profiling: config.profiling,
+        }
+      )
+    )
+  }
+
+  registerRedirects() {
+    const { config, camp } = this
+
+    // Any badge, old version. This route must be registered last.
+    camp.route(/^\/([^/]+)\/(.+).png$/, (queryParams, match, end, ask) => {
+      const [, label, message] = match
+      const { color } = queryParams
+
+      const redirectUrl = staticBadgeUrl({
+        label,
+        message,
+        color,
+        format: 'png',
+      })
+
+      ask.res.statusCode = 301
+      ask.res.setHeader('Location', redirectUrl)
+
+      // The redirect is permanent.
+      const cacheDuration = (365 * 24 * 3600) | 0 // 1 year
+      ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
+
+      ask.res.end()
+    })
+
+    if (config.redirectUri) {
+      camp.route(/^\/$/, (data, match, end, ask) => {
+        ask.res.statusCode = 302
+        ask.res.setHeader('Location', config.redirectUri)
+        ask.res.end()
+      })
+    }
+  }
+
+  async start() {
+    const {
+      bind: { port, address: hostname },
+      ssl: { isSecure: secure, cert, key },
+      cors: { allowedOrigin },
+      baseUri,
+      rateLimit,
+    } = this.config
+
+    log(`Server is starting up: ${baseUri}`)
+
+    const camp = (this.camp = Camp.start({
+      documentRoot: path.join(__dirname, '..', 'public'),
+      port,
+      hostname,
+      secure,
+      cert,
+      key,
+    }))
+
+    analytics.load()
+    analytics.scheduleAutosaving()
+    analytics.setRoutes(camp)
+
+    this.cleanupMonitor = sysMonitor.setRoutes({ rateLimit }, camp)
+
+    const { githubConstellation, metrics } = this
+    githubConstellation.initialize(camp)
+    metrics.initialize(camp)
+
+    const { githubApiProvider } = this.githubConstellation
+    suggest.setRoutes(allowedOrigin, githubApiProvider, camp)
+
+    this.registerErrorHandlers()
+    this.registerServices()
+
+    await new Promise(resolve => camp.on('listening', () => resolve()))
+  }
+
+  static resetGlobalState() {
+    // This state should be migrated to instance state. When possible, do not add new
+    // global state.
+    clearRequestCache()
+    clearRegularUpdateCache()
+  }
+
+  reset() {
+    this.constructor.resetGlobalState()
+  }
+
+  async stop() {
+    if (this.camp) {
+      await new Promise(resolve => this.camp.close(resolve))
+      this.camp = undefined
+    }
+
+    if (this.cleanupMonitor) {
+      this.cleanupMonitor()
+      this.cleanupMonitor = undefined
+    }
+
+    if (this.githubConstellation) {
+      await this.githubConstellation.stop()
+      this.githubConstellation = undefined
+    }
+
+    analytics.cancelAutosaving()
+  }
+}
diff --git a/server.spec.js b/lib/server.spec.js
similarity index 53%
rename from server.spec.js
rename to lib/server.spec.js
index 9320288b3a0fc1d62657969df163f4bcf29319a3..972cc549ef32770472585ffe356400ff63f9beae 100644
--- a/server.spec.js
+++ b/lib/server.spec.js
@@ -1,68 +1,33 @@
 'use strict'
 
 const { expect } = require('chai')
-const config = require('./lib/test-config')
-const serverConfig = require('./lib/server-config')
 const fetch = require('node-fetch')
 const fs = require('fs')
 const isPng = require('is-png')
 const isSvg = require('is-svg')
 const path = require('path')
-const serverHelpers = require('./lib/in-process-server-test-helpers')
 const sinon = require('sinon')
-const Camp = require('camp')
-const svg2img = require('./gh-badges/lib/svg-to-img')
-const { handleRequest } = require('./lib/request-handler')
-const { loadServiceClasses } = require('./services')
+const portfinder = require('portfinder')
+const svg2img = require('../gh-badges/lib/svg-to-img')
+const { createTestServer } = require('./in-process-server-test-helpers')
 
 describe('The server', function() {
-  let dummyCamp = Camp.start({ port: config.port, hostname: '::' })
-  before('Check the services', function() {
-    // The responsibility of this `before()` hook is to verify that the server
-    // will be able to register all the services. When it fails, the balance of
-    // this `describe()` block – that is, the server tests – does not run.
-    //
-    // Without this block, the next `before()` hook fails while printing this
-    // quite opaque message:
-    //
-    // Error: listen EADDRINUSE :::1111
-    //   at Object._errnoException (util.js:1022:11)
-    //   at _exceptionWithHostPort (util.js:1044:20)
-    //   at Camp.setupListenHandle [as _listen2] (net.js:1367:14)
-    //   at listenInCluster (net.js:1408:12)
-    //   at doListen (net.js:1517:7)
-    //   at _combinedTickCallback (internal/process/next_tick.js:141:11)
-    //   at process._tickDomainCallback (internal/process/next_tick.js:218:9)
-    loadServiceClasses().forEach(serviceClass =>
-      serviceClass.register({ camp: dummyCamp, handleRequest }, serverConfig)
-    )
-    dummyCamp.close()
-    dummyCamp = undefined
+  let server, baseUrl
+  before('Start the server', async function() {
+    const port = await portfinder.getPortPromise()
+    server = createTestServer({ port })
+    baseUrl = server.baseUrl
+    await server.start()
   })
-  after(function() {
-    // Free up the port and shut down the server immediately, even when the
-    // `before()` block fails during registration.
-    if (dummyCamp) {
-      dummyCamp.close()
-      dummyCamp = undefined
+  after('Shut down the server', async function() {
+    if (server) {
+      await server.stop()
     }
-  })
-
-  const baseUri = `http://127.0.0.1:${config.port}`
-
-  let server
-  before('Start the server', function() {
-    this.timeout(5000)
-    server = serverHelpers.start()
-  })
-  after('Shut down the server', function() {
-    serverHelpers.stop(server)
+    server = undefined
   })
 
   it('should produce colorscheme badges', async function() {
-    // This is the first server test to run, and often times out.
-    this.timeout(5000)
-    const res = await fetch(`${baseUri}/:fruit-apple-green.svg`)
+    const res = await fetch(`${baseUrl}/:fruit-apple-green.svg`)
     expect(res.ok).to.be.true
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -71,14 +36,13 @@ describe('The server', function() {
   })
 
   it('should produce colorscheme PNG badges', async function() {
-    this.timeout(5000)
-    const res = await fetch(`${baseUri}/:fruit-apple-green.png`)
+    const res = await fetch(`${baseUrl}/:fruit-apple-green.png`)
     expect(res.ok).to.be.true
     expect(await res.buffer()).to.satisfy(isPng)
   })
 
   it('should preserve label case', async function() {
-    const res = await fetch(`${baseUri}/:fRuiT-apple-green.svg`)
+    const res = await fetch(`${baseUrl}/:fRuiT-apple-green.svg`)
     expect(res.ok).to.be.true
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -87,7 +51,7 @@ describe('The server', function() {
 
   // https://github.com/badges/shields/pull/1319
   it('should not crash with a numeric logo', async function() {
-    const res = await fetch(`${baseUri}/:fruit-apple-green.svg?logo=1`)
+    const res = await fetch(`${baseUrl}/:fruit-apple-green.svg?logo=1`)
     expect(res.ok).to.be.true
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -96,7 +60,7 @@ describe('The server', function() {
   })
 
   it('should not crash with a numeric link', async function() {
-    const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=1`)
+    const res = await fetch(`${baseUrl}/:fruit-apple-green.svg?link=1`)
     expect(res.ok).to.be.true
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -105,7 +69,7 @@ describe('The server', function() {
   })
 
   it('should not crash with a boolean link', async function() {
-    const res = await fetch(`${baseUri}/:fruit-apple-green.svg?link=true`)
+    const res = await fetch(`${baseUrl}/:fruit-apple-green.svg?link=true`)
     expect(res.ok).to.be.true
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -114,7 +78,7 @@ describe('The server', function() {
   })
 
   it('should return the 404 badge for unknown badges', async function() {
-    const res = await fetch(`${baseUri}/this/is/not/a/badge.svg`)
+    const res = await fetch(`${baseUrl}/this/is/not/a/badge.svg`)
     expect(res.status).to.equal(404)
     expect(await res.text())
       .to.satisfy(isSvg)
@@ -123,14 +87,14 @@ describe('The server', function() {
   })
 
   it('should return the 404 html page for rando links', async function() {
-    const res = await fetch(`${baseUri}/this/is/most/definitely/not/a/badge.js`)
+    const res = await fetch(`${baseUrl}/this/is/most/definitely/not/a/badge.js`)
     expect(res.status).to.equal(404)
     expect(await res.text()).to.include('blood, toil, tears and sweat')
   })
 
   context('with svg2img error', function() {
     const expectedError = fs.readFileSync(
-      path.resolve(__dirname, 'public', '500.html')
+      path.resolve(__dirname, '..', 'public', '500.html')
     )
 
     let toBufferStub
@@ -144,7 +108,7 @@ describe('The server', function() {
     })
 
     it('should emit the 500 message', async function() {
-      const res = await fetch(`${baseUri}/:some_new-badge-green.png`)
+      const res = await fetch(`${baseUrl}/:some_new-badge-green.png`)
       // This emits status code 200, though 500 would be preferable.
       expect(res.status).to.equal(200)
       expect(await res.text()).to.include(expectedError)
@@ -153,7 +117,7 @@ describe('The server', function() {
 
   describe('analytics endpoint', function() {
     it('should return analytics in the expected format', async function() {
-      const res = await fetch(`${baseUri}/$analytics/v1`)
+      const res = await fetch(`${baseUrl}/$analytics/v1`)
       expect(res.ok).to.be.true
       const json = await res.json()
       const expectedKeys = [
diff --git a/lib/service-test-runner/cli.js b/lib/service-test-runner/cli.js
index 96b8d17635962c6e92740b6cd0e6a6293acbd051..3fa281fa6ff8297f21282f28a8868ffecbdb8a70 100644
--- a/lib/service-test-runner/cli.js
+++ b/lib/service-test-runner/cli.js
@@ -52,26 +52,39 @@
 'use strict'
 
 const minimist = require('minimist')
+const envFlag = require('node-env-flag')
 const readAllStdinSync = require('read-all-stdin-sync')
 const Runner = require('./runner')
-const serverHelpers = require('../../lib/in-process-server-test-helpers')
+const { createTestServer } = require('../../lib/in-process-server-test-helpers')
 
 require('../../lib/unhandled-rejection.spec')
 
-let server
-before('Start running the server', function() {
-  this.timeout(5000)
-  server = serverHelpers.start()
-})
-after('Shut down the server', async function() {
-  await serverHelpers.stop(server)
-})
+let baseUrl, server
+if (process.env.TESTED_SERVER_URL) {
+  baseUrl = process.env.TESTED_SERVER_URL
+} else {
+  const port = 1111
+  baseUrl = 'http://localhost:1111'
+  before('Start running the server', function() {
+    server = createTestServer({ port })
+    server.start()
+  })
+  after('Shut down the server', async function() {
+    if (server) {
+      await server.stop()
+    }
+  })
+}
 
-const runner = new Runner()
+const skipIntercepted = envFlag(process.env.SKIP_INTERCEPTED, false)
+const runner = new Runner({ baseUrl, skipIntercepted })
 runner.prepare()
+
 // The server's request cache causes side effects between tests.
-runner.beforeEach = () => {
-  serverHelpers.reset(server)
+if (!process.env.TESTED_SERVER_URL) {
+  runner.beforeEach = () => {
+    server.reset()
+  }
 }
 
 const args = minimist(process.argv.slice(3))
diff --git a/lib/service-test-runner/runner.js b/lib/service-test-runner/runner.js
index 49694d30a09fa5670ffc3df3e382b4ca45932b6f..ba7daa82b80ed59efd8d809f09aec3f363115765 100644
--- a/lib/service-test-runner/runner.js
+++ b/lib/service-test-runner/runner.js
@@ -6,6 +6,11 @@ const { loadTesters } = require('../../services')
  * Load a collection of ServiceTester objects and register them with Mocha.
  */
 class Runner {
+  constructor({ baseUrl, skipIntercepted }) {
+    this.baseUrl = baseUrl
+    this.skipIntercepted = skipIntercepted
+  }
+
   /**
    * Function to invoke before each test. This is a stub which can be
    * overridden on instances.
@@ -59,9 +64,8 @@ class Runner {
    * Register the tests with Mocha.
    */
   toss() {
-    this.testers.forEach(tester => {
-      tester.toss()
-    })
+    const { testers, baseUrl, skipIntercepted } = this
+    testers.forEach(tester => tester.toss({ baseUrl, skipIntercepted }))
   }
 }
 module.exports = Runner
diff --git a/lib/sys/monitor.js b/lib/sys/monitor.js
index 0f0050991c231bea94c88f93e02834fec7227079..9e62cb2bb213ee3d819539d1ca826d015ce165d4 100644
--- a/lib/sys/monitor.js
+++ b/lib/sys/monitor.js
@@ -2,7 +2,6 @@
 
 const secretIsValid = require('./secret-is-valid')
 const serverSecrets = require('../server-secrets')
-const config = require('../server-config')
 const RateLimit = require('./rate-limit')
 const log = require('../log')
 
@@ -17,7 +16,7 @@ function secretInvalid(req, res) {
   return false
 }
 
-function setRoutes(server) {
+function setRoutes({ rateLimit }, server) {
   const ipRateLimit = new RateLimit({
     whitelist: /^192\.30\.252\.\d+$/, // Whitelist GitHub IPs.
   })
@@ -34,7 +33,7 @@ function setRoutes(server) {
       }
     }
 
-    if (config.rateLimit) {
+    if (rateLimit) {
       const ip =
         (req.headers['x-forwarded-for'] || '').split(', ')[0] ||
         req.socket.remoteAddress
@@ -86,6 +85,12 @@ function setRoutes(server) {
       referer: refererRateLimit.toJSON(),
     })
   })
+
+  return function() {
+    ipRateLimit.stop()
+    badgeTypeRateLimit.stop()
+    refererRateLimit.stop()
+  }
 }
 
 module.exports = {
diff --git a/lib/sys/prometheus-metrics.spec.js b/lib/sys/prometheus-metrics.spec.js
index 32d1861e77567fa16108c9defe789cc066679164..e6d9759dcdcd40a1506502d651848addb0eac12d 100644
--- a/lib/sys/prometheus-metrics.spec.js
+++ b/lib/sys/prometheus-metrics.spec.js
@@ -2,32 +2,31 @@
 
 const { expect } = require('chai')
 const Camp = require('camp')
+const portfinder = require('portfinder')
 const fetch = require('node-fetch')
-const config = require('../test-config')
 const Metrics = require('./prometheus-metrics')
 
 describe('Prometheus metrics route', function() {
-  const baseUrl = `http://127.0.0.1:${config.port}`
+  let port, baseUrl
+  beforeEach(async function() {
+    port = await portfinder.getPortPromise()
+    baseUrl = `http://127.0.0.1:${port}`
+  })
 
   let camp
-  afterEach(function(done) {
+  beforeEach(async function() {
+    camp = Camp.start({ port, hostname: '::' })
+    await new Promise(resolve => camp.on('listening', () => resolve()))
+  })
+  afterEach(async function() {
     if (camp) {
-      camp.close(() => done())
+      await new Promise(resolve => camp.close(resolve))
       camp = undefined
     }
   })
 
-  function startServer(metricsConfig) {
-    return new Promise((resolve, reject) => {
-      camp = Camp.start({ port: config.port, hostname: '::' })
-      const metrics = new Metrics(metricsConfig)
-      metrics.initialize(camp)
-      camp.on('listening', () => resolve())
-    })
-  }
-
   it('returns 404 when metrics are disabled', async function() {
-    startServer({ enabled: false })
+    new Metrics({ enabled: false }).initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
@@ -36,7 +35,7 @@ describe('Prometheus metrics route', function() {
   })
 
   it('returns 404 when there is no configuration', async function() {
-    startServer()
+    new Metrics().initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
@@ -45,10 +44,10 @@ describe('Prometheus metrics route', function() {
   })
 
   it('returns metrics for allowed IP', async function() {
-    startServer({
+    new Metrics({
       enabled: true,
       allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
-    })
+    }).initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
@@ -57,10 +56,10 @@ describe('Prometheus metrics route', function() {
   })
 
   it('returns metrics for request from allowed remote address', async function() {
-    startServer({
+    new Metrics({
       enabled: true,
       allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
-    })
+    }).initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
@@ -69,10 +68,10 @@ describe('Prometheus metrics route', function() {
   })
 
   it('returns 403 for not allowed IP', async function() {
-    startServer({
+    new Metrics({
       enabled: true,
       allowedIps: '^127\\.0\\.0\\.200$',
-    })
+    }).initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
@@ -81,9 +80,9 @@ describe('Prometheus metrics route', function() {
   })
 
   it('returns 403 for every request when list with allowed IPs not defined', async function() {
-    startServer({
+    new Metrics({
       enabled: true,
-    })
+    }).initialize(camp)
 
     const res = await fetch(`${baseUrl}/metrics`)
 
diff --git a/lib/sys/rate-limit.js b/lib/sys/rate-limit.js
index 8e64f73dd618cd8abda7c81f16305756c53c1671..a3378d09015748701fb6b2044ec8eaf41f05a272 100644
--- a/lib/sys/rate-limit.js
+++ b/lib/sys/rate-limit.js
@@ -11,7 +11,12 @@ module.exports = class RateLimit {
     this.banned = new Set()
     this.bannedUrls = new Set()
     this.whitelist = options.whitelist || /(?!)/ // Matches nothing by default.
-    setInterval(this.resetHits.bind(this), this.period * 1000)
+    this.interval = setInterval(this.resetHits.bind(this), this.period * 1000)
+  }
+
+  stop() {
+    clearInterval(this.interval)
+    this.interval = undefined
   }
 
   resetHits() {
diff --git a/lib/test-config.js b/lib/test-config.js
deleted file mode 100644
index 49703320ac0dd47f403d8ee515e1ce0a16418bdf..0000000000000000000000000000000000000000
--- a/lib/test-config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-'use strict'
-
-const envFlag = require('node-env-flag')
-
-module.exports = {
-  port: 1111,
-  get testedServerUrl() {
-    return process.env.TESTED_SERVER_URL || `http://localhost:${this.port}`
-  },
-  skipIntercepted: envFlag(process.env.SKIP_INTERCEPTED, false),
-}
diff --git a/package-lock.json b/package-lock.json
index 366a39f116cfd0a1a38adf454cf2c5db29cb4f26..e8eed080d336520e87fdde96dfa9bf13de424ba8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12874,6 +12874,25 @@
       "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==",
       "dev": true
     },
+    "portfinder": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.20.tgz",
+      "integrity": "sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw==",
+      "dev": true,
+      "requires": {
+        "async": "^1.5.2",
+        "debug": "^2.2.0",
+        "mkdirp": "0.5.x"
+      },
+      "dependencies": {
+        "async": {
+          "version": "1.5.2",
+          "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+          "dev": true
+        }
+      }
+    },
     "posix-character-classes": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
diff --git a/package.json b/package.json
index 144d0e1db7a1459886dd4c31a983cb96f8e8cab9..0b302f6cba2210d957f1725595b6ba5ade911e69 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
   },
   "scripts": {
     "coverage:test:frontend": "BABEL_ENV=test nyc --nycrc-path .nycrc-frontend.json node_modules/mocha/bin/_mocha --opts mocha.opts --require @babel/polyfill --require @babel/register --require mocha-yaml-loader \"frontend/**/*.spec.js\"",
-    "coverage:test:server": "HANDLE_INTERNAL_ERRORS=false nyc node_modules/mocha/bin/_mocha --opts mocha.opts \"*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
+    "coverage:test:server": "HANDLE_INTERNAL_ERRORS=false nyc node_modules/mocha/bin/_mocha --opts mocha.opts \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
     "coverage:test:package": "nyc node_modules/mocha/bin/_mocha --opts mocha.opts \"gh-badges/**/*.spec.js\"",
     "coverage:test:integration": "nyc node_modules/mocha/bin/_mocha --opts mocha.opts \"lib/**/*.integration.js\" \"services/**/*.integration.js\"",
     "coverage:test:services": "nyc node_modules/mocha/bin/_mocha --opts mocha.opts --delay lib/service-test-runner/cli.js",
@@ -71,7 +71,7 @@
     "prettier-check": "prettier-check \"**/*.js\"",
     "danger": "danger",
     "test:js:frontend": "BABEL_ENV=test mocha --opts mocha.opts --require @babel/polyfill --require @babel/register --require mocha-yaml-loader \"frontend/**/*.spec.js\"",
-    "test:js:server": "HANDLE_INTERNAL_ERRORS=false mocha --opts mocha.opts \"*.spec.js\" \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
+    "test:js:server": "HANDLE_INTERNAL_ERRORS=false mocha --opts mocha.opts \"lib/**/*.spec.js\" \"services/**/*.spec.js\"",
     "test:js:package": "mocha --opts mocha.opts \"gh-badges/**/*.spec.js\"",
     "test:integration": "mocha --opts mocha.opts \"lib/**/*.integration.js\" \"services/**/*.integration.js\"",
     "test:services": "HANDLE_INTERNAL_ERRORS=false mocha --opts mocha.opts --delay lib/service-test-runner/cli.js",
@@ -165,6 +165,7 @@
     "node-mocks-http": "^1.7.3",
     "nyc": "^13.0.1",
     "opn-cli": "^4.0.0",
+    "portfinder": "^1.0.20",
     "prettier": "1.15.3",
     "prettier-check": "^2.0.0",
     "pretty": "^2.0.0",
diff --git a/server.js b/server.js
index 402d41c32e092d5224fbe02f125c9130a782c3a9..7ddd43b366ab7e58cac15b978be48d05811fc156 100644
--- a/server.js
+++ b/server.js
@@ -1,131 +1,19 @@
 'use strict'
 
-const path = require('path')
 const Raven = require('raven')
-
 const serverSecrets = require('./lib/server-secrets')
+
 Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install()
 Raven.disableConsoleAlerts()
 
-const { loadServiceClasses } = require('./services')
-const analytics = require('./lib/analytics')
+const Server = require('./lib/server')
 const config = require('./lib/server-config')
-const GithubConstellation = require('./services/github/github-constellation')
-const PrometheusMetrics = require('./lib/sys/prometheus-metrics')
-const sysMonitor = require('./lib/sys/monitor')
-const log = require('./lib/log')
-const { staticBadgeUrl } = require('./lib/make-badge-url')
-const makeBadge = require('./gh-badges/lib/make-badge')
-const suggest = require('./lib/suggest')
-const { makeBadgeData } = require('./lib/badge-data')
-const { handleRequest, clearRequestCache } = require('./lib/request-handler')
-const { clearRegularUpdateCache } = require('./lib/regular-update')
-const { makeSend } = require('./lib/result-sender')
-
-const camp = require('camp').start({
-  documentRoot: path.join(__dirname, 'public'),
-  port: config.bind.port,
-  hostname: config.bind.address,
-  secure: config.ssl.isSecure,
-  cert: config.ssl.cert,
-  key: config.ssl.key,
-})
-
-const githubConstellation = new GithubConstellation({
-  persistence: config.persistence,
-  service: config.services.github,
-})
-const metrics = new PrometheusMetrics(config.metrics.prometheus)
-const { apiProvider: githubApiProvider } = githubConstellation
-
-function reset() {
-  clearRequestCache()
-  clearRegularUpdateCache()
-}
-
-async function stop() {
-  await githubConstellation.stop()
-  analytics.cancelAutosaving()
-  return new Promise(resolve => {
-    camp.close(resolve)
-  })
-}
-
-module.exports = {
-  camp,
-  reset,
-  stop,
-}
-
-log(`Server is starting up: ${config.baseUri}`)
-
-analytics.load()
-analytics.scheduleAutosaving()
-analytics.setRoutes(camp)
-
-if (serverSecrets && serverSecrets.shieldsSecret) {
-  sysMonitor.setRoutes(camp)
-}
-
-githubConstellation.initialize(camp)
-metrics.initialize(camp)
-
-suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp)
-
-camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => {
-  const format = match[1]
-  const badgeData = makeBadgeData('404', query)
-  badgeData.text[1] = 'badge not found'
-  badgeData.colorscheme = 'red'
-  // Add format to badge data.
-  badgeData.format = format
-  const svg = makeBadge(badgeData)
-  makeSend(format, request.res, end)(svg)
-})
-
-camp.notfound(/.*/, (query, match, end, request) => {
-  end(null, { template: '404.html' })
-})
-
-// Vendors.
-
-loadServiceClasses().forEach(serviceClass =>
-  serviceClass.register(
-    { camp, handleRequest, githubApiProvider },
-    {
-      handleInternalErrors: config.handleInternalErrors,
-      cacheHeaders: config.cacheHeaders,
-      profiling: config.profiling,
-    }
-  )
-)
-
-// Any badge, old version. This route must be registered last.
-camp.route(/^\/([^/]+)\/(.+).png$/, (queryParams, match, end, ask) => {
-  const [, label, message] = match
-  const { color } = queryParams
-
-  const redirectUrl = staticBadgeUrl({
-    label,
-    message,
-    color,
-    format: 'png',
-  })
-
-  ask.res.statusCode = 301
-  ask.res.setHeader('Location', redirectUrl)
-
-  // The redirect is permanent.
-  const cacheDuration = (365 * 24 * 3600) | 0 // 1 year
-  ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
-
-  ask.res.end()
-})
 
-if (config.redirectUri) {
-  camp.route(/^\/$/, (data, match, end, ask) => {
-    ask.res.statusCode = 302
-    ask.res.setHeader('Location', config.redirectUri)
-    ask.res.end()
-  })
-}
+;(async () => {
+  try {
+    await new Server(config).start()
+  } catch (e) {
+    console.error(e)
+    process.exit(1)
+  }
+})()
diff --git a/services/github/auth/acceptor.spec.js b/services/github/auth/acceptor.spec.js
index 888a8fcdca03d16bdefce9723a423a71b865f101..6bb10e56fffa6ad30e899840ce030032712df5db 100644
--- a/services/github/auth/acceptor.spec.js
+++ b/services/github/auth/acceptor.spec.js
@@ -3,13 +3,12 @@
 const { expect } = require('chai')
 const Camp = require('camp')
 const got = require('got')
+const portfinder = require('portfinder')
 const queryString = require('query-string')
 const nock = require('nock')
-const config = require('../../../lib/test-config')
 const serverSecrets = require('../../../lib/server-secrets')
 const acceptor = require('./acceptor')
 
-const baseUri = `http://127.0.0.1:${config.port}`
 const fakeClientId = 'githubdabomb'
 
 describe('Github token acceptor', function() {
@@ -25,15 +24,21 @@ describe('Github token acceptor', function() {
     delete serverSecrets.shieldsIps
   })
 
+  let port, baseUrl
+  beforeEach(async function() {
+    port = await portfinder.getPortPromise()
+    baseUrl = `http://127.0.0.1:${port}`
+  })
+
   let camp
-  beforeEach(function(done) {
-    camp = Camp.start({ port: config.port, hostname: '::' })
-    camp.on('listening', () => done())
+  beforeEach(async function() {
+    camp = Camp.start({ port, hostname: '::' })
+    await new Promise(resolve => camp.on('listening', () => resolve()))
   })
-  afterEach(function(done) {
+  afterEach(async function() {
     if (camp) {
-      camp.close(() => done())
-      camp = null
+      await new Promise(resolve => camp.close(resolve))
+      camp = undefined
     }
   })
 
@@ -42,7 +47,7 @@ describe('Github token acceptor', function() {
   })
 
   it('should start the OAuth process', async function() {
-    const res = await got(`${baseUri}/github-auth`, { followRedirect: false })
+    const res = await got(`${baseUrl}/github-auth`, { followRedirect: false })
 
     expect(res.statusCode).to.equal(302)
 
@@ -57,7 +62,7 @@ describe('Github token acceptor', function() {
   describe('Finishing the OAuth process', function() {
     context('no code is provided', function() {
       it('should return an error', async function() {
-        const res = await got(`${baseUri}/github-auth/done`)
+        const res = await got(`${baseUrl}/github-auth/done`)
         expect(res.body).to.equal(
           'GitHub OAuth authentication failed to provide a code.'
         )
@@ -93,7 +98,7 @@ describe('Github token acceptor', function() {
       })
 
       it('should finish the OAuth process', async function() {
-        const res = await got(`${baseUri}/github-auth/done`, {
+        const res = await got(`${baseUrl}/github-auth/done`, {
           form: true,
           body: { code: fakeCode },
         })
diff --git a/services/github/auth/admin.spec.js b/services/github/auth/admin.spec.js
index 8ce4bcdbda04ec32a33cc461c9b28c3844f8e47e..dc16d19f3ff5fbc5a22fa965fa3720ec2c792f67 100644
--- a/services/github/auth/admin.spec.js
+++ b/services/github/auth/admin.spec.js
@@ -4,7 +4,7 @@ const { expect } = require('chai')
 const sinon = require('sinon')
 const Camp = require('camp')
 const fetch = require('node-fetch')
-const config = require('../../../lib/test-config')
+const portfinder = require('portfinder')
 const serverSecrets = require('../../../lib/server-secrets')
 const { setRoutes } = require('./admin')
 
@@ -34,16 +34,20 @@ describe('GitHub admin route', function() {
     sandbox.restore()
   })
 
-  const baseUrl = `http://127.0.0.1:${config.port}`
+  let port, baseUrl
+  before(async function() {
+    port = await portfinder.getPortPromise()
+    baseUrl = `http://127.0.0.1:${port}`
+  })
 
   let camp
-  before(function(done) {
-    camp = Camp.start({ port: config.port, hostname: '::' })
-    camp.on('listening', () => done())
+  before(async function() {
+    camp = Camp.start({ port, hostname: '::' })
+    await new Promise(resolve => camp.on('listening', () => resolve()))
   })
-  after(function(done) {
+  after(async function() {
     if (camp) {
-      camp.close(() => done())
+      await new Promise(resolve => camp.close(resolve))
       camp = undefined
     }
   })
diff --git a/services/service-tester.js b/services/service-tester.js
index 4dc243c8e211068b92c00e210f36c3da10b58d93..d04e4a7bc930e61e390e8c056a9b5cc7e02c074d 100644
--- a/services/service-tester.js
+++ b/services/service-tester.js
@@ -1,7 +1,6 @@
 'use strict'
 
 const emojic = require('emojic')
-const config = require('../lib/test-config')
 const frisby = require('./icedfrisby-no-nock')(
   require('icedfrisby-nock')(require('icedfrisby'))
 )
@@ -60,7 +59,6 @@ class ServiceTester {
   create(msg) {
     const spec = frisby
       .create(msg)
-      .baseUri(`${config.testedServerUrl}${this.pathPrefix}`)
       .before(() => {
         this.beforeEach()
       })
@@ -92,14 +90,16 @@ class ServiceTester {
   /**
    * Register the tests with Mocha.
    */
-  toss() {
-    const specs = this.specs
+  toss({ baseUrl, skipIntercepted }) {
+    const { specs, pathPrefix } = this
+    const testerBaseUrl = `${baseUrl}${pathPrefix}`
 
     const fn = this._only ? describe.only : describe
     // eslint-disable-next-line mocha/prefer-arrow-callback
     fn(this.title, function() {
       specs.forEach(spec => {
-        if (!config.skipIntercepted || !spec.intercepted) {
+        if (!skipIntercepted || !spec.intercepted) {
+          spec.baseUri(testerBaseUrl)
           spec.toss()
         }
       })