Effective Docker Healthchecks For Node.js

Writing effective healthchecks will make your services running in Kubernetes or Docker Swarm more reliable

When I first started writing healthcheck’s for Node.js, they were pretty naive. They also followed the commonly suggested wisdom of the internet. Just install curl and hit your API!

Great!

But, turns out it’s not so great.

For one, many of my services do not have API endpoints over HTTP. Most of my internal services use AMQP. I ended up adding on lightweight APIs and installing curl in my production containers just for healthchecks!

Adding more complexity to a service is a never a great solution.

Additionally, just because curl can hit your API, doesn’t mean it’s working correctly.

Maybe your API is responding but your service can’t connect to your database, or queue, or whatever other dependency it needs to be fully operational.

I really wanted to be able to TRUST my healthcheck. If it says healthy, I want to know that the ability to connect to it’s dependencies was part of that state’s computation.

In celebration of Node.js 10’s LTS release, I’ll show you how to create a custom healthcheck for your service, and it’s dependencies, by writing a simple ES6 module with .mjs!

Here we go!

Let’s imagine a service.

We still want our healthcheck to be relatively lightweight, and for my purposes, just checking that all of the dependencies are working and that I’m able to connect with them would be loads better than a curl to a endpoint that just says ‘ok’.

In my example, I’m using two libraries servicebus and sourced. These libraries are dependent on rabbitmq, mongodb, and redis.

If our service is unable to connect to any of these three, it is definitely not healthy.

Seems how these libraries can be passed a configuration with their connection options, and that’s how our application is using them internally, no need to do anything extra fancy or reinvent the wheel.

The healthcheck

Here’s the healthcheck example. I store it in ./bin/healthcheck.mjs.

#!/bin/sh
':' //# https://medium.com/@patrickleet ; exec /usr/bin/env node --experimental-modules "$0" "$@"

import servicebus from 'servicebus-bus-common'
import { config } from '../config.mjs'
import mongoClient from 'sourced-repo-mongo/mongo'

export const exit = ({ healthy = true } = {}) => {
return healthy ? process.exit(0) : process.exit(1)
}

export const check = () => {
return Promise.all([
mongoClient.connect(config.sourced.mongo.url),
servicebus.makeBus(config.servicebus)
])
}

export const handleSuccessfulConnection = (healthcheck) => {
return () => {
healthcheck({ healthy: true })
}
}

export const handleUnsuccessfulConnection = (healthcheck) => {
return (e) => {
healthcheck({ healthy: false })
}
}

check()
.then(handleSuccessfulConnection(exit))
.catch(handleUnsuccessfulConnection(exit))

And that’s it!

Let’s break it down, and then explore how we can use it in production.

#!/bin/sh
':' //# https://medium.com/@patrickleet ; exec /usr/bin/env node --experimental-modules "$0" "$@"

This first line executes /bin/sh to call node with the experimental modules flag.

This way, we can just run the file without trying to figure out how to modify our calling code to know or expect to need to use the --experimental-modules flag.

Next, we have our imports, and then define the exit function.

This is so we can call exit and say what we mean, instead of trying to remember whether we want exit code 0 or 1.

export const exit = ({ healthy = true } = {}) => {
return healthy ? process.exit(0) : process.exit(1)
}

Our actual healthcheck function then just returns an array of promises that need to be resolved for the service to be considered healthy:

export const check = () => {
return Promise.all([
mongoClient.connect(config.sourced.mongo.url),
servicebus.makeBus(config.servicebus)
])
}

Notice that I’m able to reuse the same config that my application uses.

Lastly, we call check, and provide a success and a failure function.

export const handleSuccessfulConnection = (healthcheck) => {
return () => {
healthcheck({ healthy: true })
}
}

export const handleUnsuccessfulConnection = (healthcheck) => {
return (e) => {
healthcheck({ healthy: false })
}
}

check()
.then(handleSuccessfulConnection(exit))
.catch(handleUnsuccessfulConnection(exit))

Using the healthcheck

Now, to use the healthcheck easily, we are going to do two things.

  1. Set up the bin section of package.json to expose our healthcheck command.
  2. npm link to be able to call the healthcheck

package.json

{
"bin": {
"healthcheck": "./bin/healthcheck.mjs"
},
// rest of package.json
}

Now when we npm link our project, the commands defined in bin will be available via our CLI.

This doesn’t make a ton of sense to do in our local machine, but it does inside of our container.

Here’s an example multi-stage Dockerfile which does so:

FROM node:10-alpine as build

# install gyp tools
RUN apk add --update --no-cache \
python \
make \
g++

ADD . /src
WORKDIR /src
RUN npm ci
RUN npm run lint
RUN npm run test
RUN npm prune --production
FROM node:10-alpine

ENV PORT=3010
EXPOSE 3010

COPY --from=build /src/package.json package.json
COPY --from=build /src/package-lock.json package-lock.json
COPY --from=build /src/node_modules node_modules
COPY --from=build /src/bin bin
COPY --from=build /src/handlers handlers
COPY --from=build /src/lib lib
COPY --from=build /src/config.mjs config.mjs

RUN npm link
HEALTHCHECK CMD healthcheck

CMD node --experimental-modules ./bin/start.mjs

Notice the command RUN npm link in the second stage (second FROM defines second stage).

RUN npm link
HEALTHCHECK CMD healthcheck

This makes the command healthcheck in that container, execute the healthcheck we’ve defined.

This can also be used by Kubernetes livenessProbes and readinessProbes!

Check out the probes above, and then check out the /bin folder for the healthcheck example!

Happy Hacking!

Interested in hearing my DevOps story? Read it on HackerNoon now!

If you’ve found this helpful, and would like to help me, the best way to do so is “clapping” for this article (press and hold) up to 50 times, and sharing it on social media! :)