docker nestjs meilisearch nginx image

Docker + Meilisearch + NGINX + NestJS in production

Discover how to set up a production-ready search engine using Meilisearch, Docker, NGINX and NestJS

9 min read

December 31st, 2024

Nick Mousavi

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

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

app.controller.ts
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();
  }
}

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

app.service.ts
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);
  }
}

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:

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

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;
        }
    }
}

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!

file_type_light_yaml docker-compose.yml
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:

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

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'

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"
}

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'
  • 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
}

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!

Extended Q&A: Docker + Meilisearch + NGINX + NestJS in Production

Meilisearch is a lightweight, open-source search engine designed for fast and typo-tolerant search experiences. Unlike Elasticsearch, it requires significantly less configuration and is optimized for low-latency results, making it ideal for smaller projects or applications focused on user-friendly search.

Meilisearch supports API key management for securing endpoints. By setting a masterKey in the Docker environment, you can create scoped API keys with specific permissions (e.g., search-only access) and expiration dates for added security.

Docker simplifies deployment by containerizing applications, ensuring consistent environments, and enabling scalable service orchestration through tools like Docker Compose. It reduces compatibility issues and eases the management of dependencies.

NGINX enhances security by hiding internal services, improves performance with request routing and caching, and simplifies management by consolidating multiple services under one domain. It also handles SSL termination if required.

NestJS offers a structured framework to build scalable server-side applications. It simplifies creating endpoints for tasks like API key generation, seeding data, and integrating with Meilisearch through its modular and dependency injection features.

  • Scaling: Ensuring Meilisearch handles high query volumes may require resource adjustments.
  • Data Consistency: Keeping the Meilisearch index synchronized with your primary database.
  • Monitoring and Logging: Setting up tools like Prometheus or Elasticsearch-Logstash-Kibana (ELK) for real-time monitoring.
  • SSL and Security: Configuring SSL certificates for NGINX and securing API keys.

Yes! By integrating WebSockets or similar technologies in your NestJS application, you can push real-time updates to the front-end. Meilisearch’s fast indexing ensures these updates reflect almost instantly.

  • Basic Knowledge: Familiarity with Docker, NGINX, and NestJS.
  • Environment: A machine with Docker installed.
  • Access: Configured DNS and SSL certificates if deploying publicly.
  • Data: An understanding of your dataset’s structure for indexing and searching efficiently.
Resources and References

Github Repository

MeiliSearch JS Client

MeiliSearch Docs

NGINX upstream

Docker Compose

NestJS Docs

Docker

Nick Mousavi
A small step forward is still progress.

To get in touch with Nick Mousavi,
please enter your email address below.

© 2020-present Nick Mousavi. All Rights Reserved.

Privacy Policy