Docker Container Roles Pattern for NestJS Apps

Dmitry Khorev
5 min readAug 27, 2022

--

Docker and NestJS development
Docker and NestJS development

In this story, I will show you how I build small-to-medium-sized NestJS applications using a hybrid monolith pattern.

I have one repository, and one docker container with the whole app, but each container is a separate application, almost like it was deployed from another repo!

This is achieved by container role pattern and NestJS’s fantastic module architecture design.

Objective

Launch event-driven monolith NestJS application using just a single container image and repository.

My application will have an API route for math operation add and will consume two arguments A and B.

Math add operation and its arguments will be queued and processed by a background worker node. The result will be displayed in the instance log (real-world — database, external service, WebSocket broadcast — whatever).

This application will have one cron job that runs minutely and logs its result in the worker’s console.

Solution

We’ll build a container that can run our NestJS app and its dependencies, but it will have a different module entry point based on the container’s role in the stack.

Our example app will have those roles:

  • API service to ingest data
  • Service to manage queues
  • Service to run cron jobs

Our goal is to build this kind of architecture but with only 1 NestJS application.

Target NestJS stack overview
Target NestJS stack overview

Example application

Demo code for this application: https://github.com/dkhorev/docker-container-roles-pattern-nestjs

This is how the project folder would look like

NestJS app folder structure
NestJS app folder structure

common — module that will provide storage connections and Models for all our sub-modules.

api — a module that provides an API route for data ingestion and queues the jobs

workers — a module that is responsible for processing queued jobs

cron — a module that will run timed tasks for us and also has access to start queued jobs (for workers to pick up)

Next, let’s look over the most interesting connecting parts from our modules.

API module

API controller

API controller receives A and B and adds a job to the math-add queue.

Cron module

Cron service emits 1 event every minute

My sample cron service only has 1 schedule: add a queued job to the cron-jobs queue every minute. We just pass a random string and will confirm it is received on the worker’s instance.

Workers module

This module has 2 queue processors

The processor responsible for “math-add” operations
The processor responsible for “cron-jobs” operations

As you can see this setup is pretty simple, nonetheless, it has integrated Redis and Node.js Bull package to work via queues messaging. A good starting point for any similar async application.

Now let’s create our container.

Building the container

Dockerfile for NestJS app

Nothing new here, it’s a basic multi-stage Node.js container.

Container entry point

This is the part where our app decides what role it is started with, and what modules should it load.

Map of container roles and their bootstrap scripts

This file includes a map of roles with corresponding bootstrap functions returned.

api — starts the application with ApiModule, Fastify adapter, and listens on a PORT

cron — starts a standalone application with CronModule, not listening for requests

workers — again, just a standalone application with WorkersModule

Demo docker-compose file

This is what our service map looks like with the docker-compose file.

Docker compose yaml for deploying NestJS app

Notice every service that has image: role-app:latest has a CONTAINER_ROLE set.

API and Workers services are replicated to run 2 instances each to simulate scaled deployment.

Running the demo

I recommend building and pulling the required images first.

We can start our stack by running this command:

docker-compose up -d

Docker stack is now running with required setup
Docker stack is now running with the required setup

We can trigger our API with simple curl requests via console:

curl http://127.0.0.1:8181/api/add/1/10

curl http://127.0.0.1:8181/api/add/2/3

curl http://127.0.0.1:8181/api/add/4/7

curl http://127.0.0.1:8181/api/add/2/2

Now check workers logs with docker logs docker-container-roles-pattern-nestjs_workers_1 or docker logs docker-container-roles-pattern-nestjs_workers_2. Jobs are distributed between them so your results may vary.

Worker 1 logs
Worker 1 logs
Worker 2 logs
Worker 2 logs

As you can see our workers app has already processed a few scheduler jobs and API requests.

Check scheduler logs: docker logs docker-container-roles-pattern-nestjs_cron_1

Cron service has already queued some jobs
Cron service has already queued some jobs

Now check what the app’s API instance is doing: docker logs docker-container-roles-pattern-nestjs_api_1

API instance only receives requests and queues jobs
API instance only receives requests and queues jobs

Conclusion

We have seen from the screenshots above — our monolith NestJS application can be launched with separate scalable services by just passing CONTAINER_ROLE to each of them.

If you have a strong team, that can extend this project in a modular way (thanks NestJS!), then I found this approach can be effective.

You get a taste of building microservice-like architecture without the pitfalls it has for inexperienced engineering teams.

Effectively you have a set of microservices running from a single monolith repo:

  • API layer
  • Workers pool
  • Cron scheduler

Possible to add with the same approach:

  • Frontend layer (nginx + any SPA)
  • DB migrator
  • Any container to run custom commands/scripts
  • Provide secret keys only to containers that need them

Demo code for this application: https://github.com/dkhorev/docker-container-roles-pattern-nestjs

If you’re interested in PHP, then check this article that has a demo of the same pattern applied with Laravel example: https://medium.com/@dkhorev/docker-container-roles-pattern-for-laravel-apps-d445a62d230f

--

--

Dmitry Khorev

Sharing my experience in software engineering (NestJS, Laravel, System Design, DevOps)