Skip to main content

Deploy to Kubernetes


At this point, it is expected, that you are familiar at least with the basics of Kubernetesopen in new window. There are some good resources to learn, how Node.js programs can be deployed in a Kubernetes cluster:

In the flowing example, it is expected:

  • you have a mono-repo with one service TheService
  • typescript is listed devDependencies in your package.json
  • you have in your tsconfig.json: in compilerOptions the outDir set to build
  • you have in your tsconfig.json: include set to ["./src/index.ts"]


Here, we only focus on technical requirements and basic setup. The example can be found on GitHub PURISTA examplesopen in new window.

Prepare your code

Kubernetes is normally used by microservices, which are providing HTTP endpoints. Also, it is expected, that the service provides liveness and readiness probes over HTTP. Therefore, we use a small HTTP server here.

We also want to handle shutdown signals properly.

It can be done by using @purista/k8s-sdk.


The @purista/k8s-sdk package is using Honoopen in new window to provide a modern, flexible and lightweight http server.
Because of this, the webserver is able to use the benefits of different runtime environments like Bunopen in new window.
See Honoopen in new window

As you will see, you can optional expose commands as HTTP endpoints. This will allow integration into existing or other microservices environments or exposing commands as HTTP endpoints for clients.

Here is a full example, of how the index file might look like, if you want to deploy a service to Kubernetes. You can adjust this example for your actual requirements.

Node.js package required

If you use Node.js as runtime, you need to install the additional package @hono/node-server with version 1.0.0 or higher!

// src/index.ts
// For running on Node.js a small additional package is needed:
import { serve } from '@purista/hono-node-server'

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { AmqpBridge } from '@purista/amqpbridge'
import {
} from '@purista/core'
import { getHttpServer } from '@purista/k8s-sdk'

import { theServiceV1Service } from './service/theService/v1/'

const main = async () => {
  // create a logger
  const logger = initLogger('debug')

  // add listeners to log really unexpected errors
  process.on('uncaughtException', (error, origin) => {
    const err = UnhandledError.fromError(error)
    logger.error({ err, origin }, `unhandled error: ${err.message}`)

  process.on('unhandledRejection', (error, origin) => {
    const err = UnhandledError.fromError(error)
    logger.error({ err, origin }, `unhandled rejection: ${err.message}`)

  // optional: set up opentelemetry if you like to use it
  const exporter = new OTLPTraceExporter({
    url: `http://localhost:14268/api/traces`,
  const spanProcessor = new SimpleSpanProcessor(exporter)

  // optional: set up stores if they are needed for your service
  const secretStore = new DefaultSecretStore({ logger })
  const configStore = new DefaultConfigStore({ logger })
  const stateStore = new DefaultStateStore({ logger })

  // set up the eventbridge and start the event bridge
  const eventBridge = new AmqpBridge({
    instanceId: process.env.HOSTNAME || getNewInstanceId(),
    config: {
      url: process.env.AMQP_URL,
  await eventBridge.start()

  // set up the service
  const theService = theServiceV1Service.getInstance(eventBridge, {
  await theService.start()

  // create http server
  const app = getHttpServer({
    // check event bridge health if /healthz endpoint is called
    healthFn: () => eventBridge.isHealthy(),
    // optional: expose the commands if they are defined to have url endpoint
    services: theService,
    // optional: expose service endpoints at [apiMountPath]/v[serviceVersion]/[path defined for command]
    // defaults to /api
    apiMountPath: '/api',

  // start the http server
  // defaults to port 3000
  // optional: you can set the `port` in the optional parameter of this method
  // use the `serve` method form the `@purista/hono-node-server` package
  const server = serve({
    fetch: app.fetch,

  // register shut down methods
  gracefulShutdown(logger, [
    // start with the event bridge to no longer accept incoming messages
    // optional: shut down the service
    // optional: shut down the secret store
    // optional: shut down the config store
    // optional: shut down the state store
      name: 'httpserver',
      destroy: async () => {


With this setup, you should be able to build and deploy your app as a container in Kubernetes like any other node-based service.

Build a docker image

To get a docker image, which then can be deployed, you will need to have done two things:

  • compile the typescript code base to plain JavaScript
  • create a docker file with minimum resources (no dev dependencies) and compiled JavaScript

Luckily, we can do it in one big step, by using docker's multi-stage buildsopen in new window

Place a Dockerfile into the root of your repository. The file looks something like this.

FROM node:18-alpine as builder

RUN mkdir -p /app

# should be improved by you depending on your needs
# use a .dockerignore file
COPY . .

RUN npm ci

RUN npx tsc
# or you can use esbuild
# RUN npx esbuild ./src/index.ts --bundle --platform=node --outfile=build/src/index.js

FROM node:18-alpine as app

ENV NODE_ENV=production

RUN mkdir -p /app
COPY --chown=node:node --from=builder /app/package.json /app
COPY --chown=node:node --from=builder /app/build /app

RUN npm install --omit=dev

# exposed port must match the one used to start the http server in src/index.ts
ENTRYPOINT ["node", "index.js"]


Please adjust this example to your needs.
You should improve it, by only copying needed things.

Now, it's time to build the image.
To do so, run docker build . -t the-service:v1, which will create a docker imaged named TheService with the tag v1.
You can adjust the naming and tagging to your preferred way.

Because the image is currently only available on your local machine, you need to push it to a registry.
Kubernetes will then be able, to pull the image from the registry.
Which registry is used, depends on your project and environment.

Here is a basic deployment.yaml file for Kubernetes.

apiVersion: apps/v1
kind: Deployment
    name: theServiceV1
        app: theServiceV1
    replicas: 2
            app: theServiceV1
                app: theServiceV1
              - name: theServiceV1
                image: the-service:v1
                imagePullPolicy: IfNotPresent
                    path: /healthz
                    port: 8080
                  initialDelaySeconds: 10
                  periodSeconds: 10

                    path: /healthz
                    port: 8080
                  initialDelaySeconds: 5
                  periodSeconds: 10


Please be aware, that this is just an example for demonstration and local development purpose.
You should adjust it for production, depending on your actual environment.

That's it?

Well, you might want to define a (Kubernetes) service, which makes Pods accessible to other Pods or users outside the cluster.

But, here we only focus on the PURISTA related stuff, and not go into details of Kubernetes. There are a bunch of good articles, documentations, how-to's, which cover the Kubernetes and infrastructure stuff a way better.

Add custom endpoints

There might be the need, that you want to add some custom endpoints.
As an example, in 2.1 Service - Advanced we add Prometheusopen in new window to our service. To allow Prometheus to collect the data, we need an additional /metrics endpoint.

We can simply extend our file /src/index.ts, to provide the endpoint /metrics

// create http server
const server = getHttpServer({
  // check event bridge health if /healthz endpoint is called
  healthFn: () => eventBridge.isHealthy(),
  // optional: expose the commands if they are defined to have url endpoint
  services: theService,
  // optional: expose service endpoints at [apiMountPath]/v[serviceVersion]/[path defined for command]
  // defaults to /api
  apiMountPath: '/api',

// add the metrics route
server.router.add('GET', '/metrics', async (_request, response) => {
  response.setHeader('Content-Type', register.contentType)
  response.end(await register.metrics())

The new endpoint /metrics can now be added to the deployment.yaml file for Kubernetes.

You can follow updated on Twitter @purista_jsopen in new window or join the Discord serveropen in new window to get in touch with PURISTA maintainers and other developers.

Last update:
Contributors: Sebastian Wessel