DEV Community

Nick Mousavi
Nick Mousavi

Posted on • Originally published at biomousavi.com

Building a Production Stack: Docker, Meilisearch, NGINX & NestJS

Introduction

If you’re reading this, chances are you’re already familiar with Elasticsearch and are now curious about Meilisearch. So, no need to start from the basics, let’s dive right into why Meilisearch might be the better choice!

  1. Simplicity: Meilisearch has 90% fewer configuration options compared to Elasticsearch, meaning less time spent learning and setting up. For example, while Elasticsearch often requires 5–10 configurations just to index and search efficiently, Meilisearch works well out of the box with minimal tweaks. 😎

  2. Performance: Meilisearch is designed for instant search and is optimized for low-latency results, typically under 50ms, which makes it feel snappier for end users. Elasticsearch, while powerful, often requires extra optimization to reach similar response times for smaller datasets.

  3. Typo Tolerance: Meilisearch provides built-in typo tolerance without extra configuration, improving the user experience significantly.

If your use case doesn’t require massive scalability or advanced querying, Meilisearch is the simpler, faster, and more cost-effective option!

and if you are a Rustacean 🦀, yeah it is written in Rust language.

What’s the Plan?

We’re going to create a Nest application to seed Meilisearch, set up Meilisearch itself, and configure NGINX as reverse proxy in front of it.

NestJS Application

  • The repository with the complete code is linked at the end of this article.

Start by preparing your NestJS application.

Install the Meilisearch JavaScript Client:

npm install meilisearch
Enter fullscreen mode Exit fullscreen mode

Now we want to create an endpoint to generate apiKey for front-end application, so we need a controller like this

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('/')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('api-key')
  async createApiKey() {
    return this.appService.createApiKey();
  }
}

Enter fullscreen mode Exit fullscreen mode

then we need to add the functionality to our module service:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { MeiliSearch } from 'meilisearch';

@Injectable()
export class AppService implements OnModuleInit {
  private meiliClient: MeiliSearch;
  private index = 'movies';

  constructor() {
    // Create new Meilisearch Instance
    this.meiliClient = new MeiliSearch({
      host: 'http://meilisearch:7700',
      apiKey: 'masterKey',
    });

    // Check MeiliSearch Health
    this.meiliClient.health().then(console.log);
  }

// Seed Meilisearch
  async onModuleInit() {
    await this.seed();
  }

// Generate API key for front-end search
  async createApiKey() {
    const key = await this.meiliClient.createKey({
      expiresAt: null,
      indexes: [this.index],
      name: 'Search API Key',
      actions: ['search', 'documents.get'],
      description: 'Use it to search from the frontend code',
    });

    return key;
  }

// Seeding Functionality
  private async seed() {
    const index = this.meiliClient.index(this.index);

    const documents = [
      { id: 1, title: 'Nick Mousavi', genres: ['Romance', 'Drama'] },
      { id: 2, title: 'Wonder Woman', genres: ['Action', 'Adventure'] },
      { id: 3, title: 'Life of Pi', genres: ['Adventure', 'Drama'] },
      { id: 4, title: 'Mad Max: Fury Road', genres: ['Adventure'] },
      { id: 5, title: 'Moana', genres: ['Fantasy', 'Action'] },
      { id: 6, title: 'Philadelphia', genres: ['Drama'] },
    ];

    await index.addDocuments(documents);
  }
}
Enter fullscreen mode Exit fullscreen mode

MeiliSearch Option Object:

  • apiKey: 'masterKey' is the api key that we will configure in meilisearch service in docker-compose.yml file.
  • host: 'http://meilisearch:7700' is the Meilisearch address in our docker network.

createKey method options:

  • expiresAt: When the key becomes invalid (null means never expires)
  • indexes: Which Meilisearch indexes this key can access
  • name: Key identifier for your reference
  • actions: Permissions (search: can search, documents.get: can fetch individual documents)
  • description: Note about key's purpose

Now your NestJS application is ready to connect to MeiliSearch server and generate API keys.

Dockerizing

Here, we'll create a Dockerfile to containerize our NestJS application for production. At the root of your NestJS project, create a file named Dockerfile:

FROM node:22.12.0 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build && npm prune --production



FROM gcr.io/distroless/nodejs22-debian12 AS production
ENV NODE_ENV=production
WORKDIR /app 
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json

Enter fullscreen mode Exit fullscreen mode

Perfect, now our NestJS production build is ready to add to our docker-compose.yml file.

NGINX Configuration

We'll use Nginx as a reverse proxy to route requests to our API and Meilisearch services in the Docker Compose setup. Create an nginx.conf file at the root of the project and add the following content:

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;

    upstream api {
        server api:3000;
    }

    upstream meilisearch {
        server meilisearch:7700;
    }

    server {
        listen 80;

        # API endpoints
        location /api/ {
            proxy_pass http://api/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Meilisearch endpoints
        location /search/ {
            proxy_pass http://meilisearch/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now your NGINX webserver is ready to route requests to api and meilisearch services.

Docker Compose

We’ll use Docker Compose to manage all the services (API, NGINX, and Meilisearch) together.

Start by creating a docker-compose.yaml file at the root of the project and add the following content. Don’t worry, I’ll explain each service step by step!

services:

  # Nestjs API Service
  api:
    build:
      context: .
      target: production
      dockerfile: ./Dockerfile
    restart: unless-stopped
    networks:
      - biomousavi
    depends_on:
      - meilisearch
    ports:
      - 3000:3000
    command: 'dist/main.js'

  # Meilisearch Service
  meilisearch:
    image: getmeili/meilisearch:v1.12
    volumes:
      - meilisearch-volume:/meili_data
    ports:
      - '7700:7700'
    environment:
      - MEILI_MASTER_KEY=masterKey
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:7700']
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - biomousavi

  # Nginx Service
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    networks:
      - biomousavi
    depends_on:
      - api
      - meilisearch

networks:
  biomousavi:
    driver: bridge

volumes:
  meilisearch-volume:
Enter fullscreen mode Exit fullscreen mode

This Docker Compose file defines three services:

  1. API Service: Runs the NestJS application in production mode, exposing it on port 3000. It depends on the Meilisearch service to start first.

  2. Meilisearch Service: Runs Meilisearch for search functionality, storing its data in a persistent volume and secured with a master key. It listens on port 7700.

  3. Nginx Service: Acts as a reverse proxy, routing requests to the API and Meilisearch. It uses a custom Nginx configuration and exposes port 80 for external access.

Your file structure should be something similar to this:

docker-nginx-meilisearch-nest-folder-structure

Run The Application

To build and run the application, use one of the following commands:

docker compose up --build
#OR run in detached mode
docker compose up --build -d
Enter fullscreen mode Exit fullscreen mode

Generate a New API Key

In your client application, send a GET request to obtain a new API key for search requests:

curl 'http://localhost/api/api-key'
Enter fullscreen mode Exit fullscreen mode

This request calls the NestJS API and returns a JSON response like this:

{
  "name": "Search API Key",
  "description": "Use it to search from the frontend code",
  "key": "a5e62c45497396bb1b0535b4cc4b84dec37713c5bdb4b78f18624af6f33ebac7",
  "uid": "82f8a1a4-77cd-481d-9fcb-21fdde13246f",
  "actions": ["search", "documents.get"],
  "indexes": ["movies"],
  "expiresAt": null,
  "createdAt": "2024-12-31T18:06:45.190728957Z",
  "updatedAt": "2024-12-31T18:06:45.190728957Z"
}
Enter fullscreen mode Exit fullscreen mode

Perform a Search Request

Use the obtained API key (key) to search your index. Add it as a bearer token in the request:

curl 'http://localhost/search/indexes/movies/search?q=mad+max' -H 'Authorization: Bearer your_api_key'
Enter fullscreen mode Exit fullscreen mode
  • Replace your_api_key with the key from the previous response.

It sends request to Meilisearch service.

The response should be similar to the following JSON:

{
  "hits": [
    {
      "id": 4,
      "title": "Mad Max: Fury Road",
      "genres": [
        "Adventure"
      ]
    }
  ],
  "query": "mad max",
  "processingTimeMs": 8,
  "limit": 20,
  "offset": 0,
  "estimatedTotalHits": 1
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

By combining Docker, Meilisearch, NGINX, and NestJS, you can set up a scalable, production-ready search solution with minimal configuration. This guide should help you streamline the process of creating, deploying your search engine. Whether you're optimizing for performance or simplicity, this stack provides a solid foundation for your next project.

If you found this guide helpful ❤️ and saved you time, I'd be incredibly grateful if you could show some support by giving the repository a ⭐ star on GitHub!

Top comments (0)