Docker: Initialize custom users and databases in MongoDb

Docker: Initialize custom users and databases in MongoDb

Purpose

There are situations, when you need quick and re-deployable database that doesn't need to be installed on your main OS or at external host in pre-production state, having all pre-production databases, users and tables or schema ready for development.

Since there is no real need to have full cluster working on K8s for this, it's easier to deploy e.g. MongoDB with docker compose and remove it or reset into 'default state' during development process.

Theory

Imagine you have to work with an ETL that is going to fetch data from e.g. SQL Server and store it in MongoDB.

For this you would have database that you can spin whenever you want, having always its default databases in initial state.

Of course, you can create tables, databases on fly during ETL startup, but you can always create pre-prod databases setup within the compose-file.

First things first

First, you need to create space/folder:

cd /home/user/repos
mkdir mongodb-dev

cd mongodb-dev

And you need to have Docker and Docker Compose installed on your OS.

Docker compose

It is common practice, to use docker compose for creating stacks that can be easily started or recreated, depending on our needs.

First, we need to createdocker-compose.yml file, which is ours starting point:

cd /home/user/repos/mongodb-dev

touch docker-compose.yml

Then we can start editing file within our IDE/Code Editor (e.g. Visual Studio Code):

code docker-compose.yml

Our simple MongoDb setup, will use one service, with two persistent volumes for:

  • /data/
  • and /logs

The first paragraph in docker-compose.yml is always about version of compose file that is going to be used by engine.

About versioning, you can read more up here.

For this article, I will use version: '3.7'.

The initial base of our compose file looks like this:

version: '3.7'

services:
  mongodb:
    image: mongo:latest
    restart: on-failure
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT:-mongodb}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-mongodb}
      MONGO_INITDB_ROOT_DATABASE: ${MONGO_ROOT_DB:-mongodb}
    networks:
      - services
    ports:
      - "27017:27017"
    volumes:
      - mongodb-data:/data/db
      - mongodb-log:/var/log/mongodb

volumes:
  mongodb-data:
    driver: local
  mongodb-log:
    driver: local

networks:
  services:
    name: ${MONGO_NETWORK:-mongodb.network}

Were we create service with a name mongodb, that will use latest mongo image from DockerHub.

We set default user using built in variables, that are mapped to our custom ENV's:

  • MONGO_INITDB_ROOT_USERNAME
  • MONGO_INITDB_ROOT_PASSWORD
  • MONGO_INITDB_ROOT_DATABASE

In the last part initialize network and setup ports forwarding and map data inside container into external storage/volume to allow data persistence (I think it's rather self explaining).

This trick with ${VARIABLE:-secret} allows us to spin database without having proper .ENV. In other words, if there is not .env given in root directory, or variables are empty -docker-compose will use default secrets stored in plain yaml. It's good for development purposes, but storing any default passwords in plan code, is not allowed in production.

Initialize databases and users at stack startup

In order to add custom startup databases within its users, we need to create .sh file in our project directory:

touch mongo-init.sh

Then we map file to docker-entrypoint by adding to volumes this line:

volumes:
  - ./mongo-init.sh:/docker-entrypoint-initdb.d/mongo-init.sh:ro

Then we add our users passwords into environment section e.g.:

SALES_PASSWORD: ${SALES_PASSWORD:-sales}
WAREHOUSE_PASSWORD: ${WAREHOUSE_PASSWORD:-warehouse}

And place proper self-exploratory code in .sh file in order to create databases based on our variables:

set -e

mongo <<EOF
db = db.getSiblingDB('sales')

db.createUser({
  user: 'sales',
  pwd: '$SALES_PASSWORD',
  roles: [{ role: 'readWrite', db: 'sales' }],
});
db.createCollection('receipts')
db.createCollection('documents')
db.createCollection('invoices')

db = db.getSiblingDB('warehouse')

db.createUser({
  user: 'warehouse',
  pwd: '$WAREHOUSE_PASSWORD',
  roles: [{ role: 'readWrite', db: 'warehouse' }],
});
db.createCollection('documents')
db.createCollection('stocks')
db.createCollection('invoices')
db.createCollection('orders')

EOF

Final code

Final docker-compose.yml file, should look like this:

version: '3.7'

services:
  mongodb:
    image: mongo:latest
    restart: on-failure
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT:-mongodb}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-mongodb}
      MONGO_INITDB_ROOT_DATABASE: ${MONGO_ROOT_DB:-mongodb}
      SALES_PASSWORD: ${SALES_PASSWORD:-sales}
      WAREHOUSE_PASSWORD: ${WAREHOUSE_PASSWORD:-warehouse}
    networks:
      - services
    ports:
      - "27017:27017"
    volumes:
      - ./mongo-init.sh:/docker-entrypoint-initdb.d/mongo-init.sh:ro
      - mongodb-data:/data/db
      - mongodb-log:/var/log/mongodb

volumes:
  mongodb-data:
    driver: local
  mongodb-log:
    driver: local

networks:
  services:
    name: ${MONGO_NETWORK:-mongodb.network}

Running our sample database

In order to initialize it, we type (at our project root):

docker compose up -d

In order to start or stop:

docker compose stop
docker compose start

In order to remove containers:

docker compose down

In order to remove containers together with volumes, we type:

docker compose down -v

Result

If everything is okay, we should see something like this:

image.png

And be able to join our instance with users and passwords given in compose-file.