Skip to content

Tutorial

John Biundo edited this page Aug 24, 2019 · 11 revisions

Overview

This tutorial is broken down into several parts:

You can get the source code for the basic installation here.

You can see a working CodeSandbox here.

Hint Test modifying environment variables in CodeSandbox using the Secret Keys feature available under the Control Container menu.

Basic Installation

Start by creating a new NestJS app:

nest new configtut

Change directories to the configtut folder:

cd configtut

Install the necessary packages

npm install @nestjsplus/config

npm install cross-env -D

Generate a config module and service

nest g module config

nest g service config

Set up your configModule. Replace the default contents with the following code.

// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigManagerModule } from '@nestjsplus/config';

@Global()
@Module({
  imports: [
    ConfigManagerModule.register({
      useFile: 'config/development.env',
      allowExtras: true,
    }),
  ],
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

Set up your configService. Replace the default contents with the following code:

// src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigManager } from '@nestjsplus/config';
import * as Joi from '@hapi/joi';

@Injectable()
export class ConfigService extends ConfigManager {
  provideConfigSpec() {
    return {
      DB_HOST: {
        validate: Joi.string(),
        default: 'localhost',
      },
      DB_PORT: {
        validate: Joi.number()
          .min(5000)
          .max(65535),
        default: 5432,
      },
      DB_USERNAME: {
        validate: Joi.string(),
        required: true,
      },
      DB_PASSWORD: {
        validate: Joi.string(),
        required: true,
      },
      DB_NAME: {
        validate: Joi.string(),
        required: true,
      },
    };
  }
}

Create a config folder at the top level of your project (e.g., configtut/config), and create a development.env file in that folder. Add the following contents to the development.env file:

// config/development.env
DB_USERNAME=john
DB_PASSWORD=mypassword
DB_NAME=mydb
EXTRA=some-extra-data

Modify AppService to make use of your new ConfigService. Replace the default contents with the following code:

// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config/config.service';

@Injectable()
export class AppService {
  private userName: string;

  constructor(private readonly configService: ConfigService) {
    this.userName = configService.get<string>('DB_USERNAME');
  }

  getHello(): string {
    console.log(this.configService.trace());
    return `Hello ${this.userName}`;
  }
}

Run the application and examine the log output.

npm run start:dev

You should see:

  • A response like Hello john in your browser
  • Log output showing a resolveMap, similar to that shown below.

Now would be a good time to review the API and Module Configuration sections of the documentation if you have questions about the results.

0] { DB_HOST:
[0]    { dotenv: '--',
[0]      env: '--',
[0]      default: 'localhost',
[0]      resolvedFrom: 'default',
[0]      isExtra: false,
[0]      resolvedValue: 'localhost' },
[0]   DB_PORT:
[0]    { dotenv: '--',
[0]      env: '--',
[0]      default: 5432,
[0]      resolvedFrom: 'default',
[0]      isExtra: false,
[0]      resolvedValue: 5432 },
[0]   DB_USERNAME:
[0]    { dotenv: 'john',
[0]      env: '--',
[0]      default: '--',
[0]      resolvedFrom: 'dotenv',
[0]      isExtra: false,
[0]      resolvedValue: 'john' },
[0]   DB_PASSWORD:
[0]    { dotenv: 'mypassword',
[0]      env: '--',
[0]      default: '--',
[0]      resolvedFrom: 'dotenv',
[0]      isExtra: false,
[0]      resolvedValue: 'mypassword' },
[0]   DB_NAME:
[0]    { dotenv: 'mydb',
[0]      env: '--',
[0]      default: '--',
[0]      resolvedFrom: 'dotenv',
[0]      isExtra: false,
[0]      resolvedValue: 'mydb' },
[0]   EXTRA:
[0]    { dotenv: 'some-extra-data',
[0]      env: '--',
[0]      default: '--',
[0]      resolvedFrom: 'dotenv',
[0]      isExtra: true,
[0]      resolvedValue: 'some-extra-data' } }

Dockerize the basic app

Pre-requisite: This section assumes you have a Docker environment installed. It walks you through some basic Docker steps, but is not a general purpose Docker tutorial. Don't worry - you don't have to be a Docker expert and these steps are pretty quick and easy, but you do have to have Docker running.

See Docker for Windows or Docker for Ubuntu if you don't already have it installed.

To see how NestJSConfigManager works with Docker, we're going to deploy our basic app in a Docker container.

Create a Dockerfile in your project root (e.g., configtut/Dockerfile). Add the contents below to that file. This Dockerfile uses a base image called unvjohn/nestjs-config that makes testing the functionality fast and easy. The main features of the base image are:

  • Uses the phusion/baseimage Docker image which provides a nice starting point for Node based Docker apps. Read more about phusion/baseimage here
  • Pre-installs the latest NestJS version
  • Pre-installs the latest NestJsConfigManager version
# Dockerfile for NestjsConfigManager tutorial
######################################################
# BUILD
######################################################
FROM  unvjohn/nestjs-config:latest
LABEL MAINTAINER="johnfbiundo@gmail.com"

# Env Vars
# SRV_ROOT: root folder for the app module
# SRV_PORT: container will listen on SRV_PORT
#           map this to a host port in compose file or docker run command
#           (e.g., -p <host:container>)
ENV SRV_ROOT='/server' \
  SRV_PORT=3000

# Turn on NestJSConfigManager debugging
ENV DEBUG=cfg

WORKDIR $SRV_ROOT

# Remove the typescript starter Nest app from dist.  This was just there so
# we could test the base image.
RUN rm -r dist
RUN mkdir dist

# Install libs
COPY package.json .
RUN npm install

# Copy our app to dist
COPY dist dist

# Copy our production env file
COPY ./config/production.env ./config/production.env

# Listen on SRV_PORT
EXPOSE $SRV_PORT

Create a .dockerignore file and add the following contents

node_modules*
.dockerignore
test
.git
.git*
.vscode
src
views
log*
README*
.gitignore
.prettierrc
jest.config.js
nodemon.*
ormconfig.*
package-lock.json
tsconfig.*
tslint.*

Create a production.env file in the same configtut/config folder, and add the following contents:

DB_USERNAME=myProdUser
DB_PASSWORD=myProdPassword
DB_NAME=myProdDb

Update npm scripts by replacing the scripts key portion of your package.json file with the following contents. This will make it more convenient to iteratively build and test your app and Docker image.

  "scripts": {
    "prebuild:docker": "npm run build",
    "build:docker": "docker build -t configtut .",
    "build": "tsc -p tsconfig.build.json",
    "format": "prettier --write \"src/**/*.ts\"",
    "start": "ts-node -r tsconfig-paths/register src/main.ts",
    "start:dev": "cross-env NODE_ENV=development DEBUG=cfg concurrently --handle-input \"wait-on dist/main.js && nodemon\" \"tsc -w -p tsconfig.build.json\" ",
    "start:debug": "nodemon --config nodemon-debug.json",
    "prestart:prod": "rimraf dist && npm run build",
    "start:prod": "node dist/main.js",
    "start:docker": "docker rm -f configtut; docker run --rm --name configtut -it -p 3000:3000 configtut /sbin/my_init -- bash -l",
    "start:docker-testenv": "docker rm -f configtut; docker run --rm --name configtut -it -e 'DB_USERNAME=realProdUser' -e 'DB_PASSWORD=realProdPassword' -e 'DB_NAME=realProdDb' -p 3000:3000 configtut /sbin/my_init -- bash -l",
    "lint": "tslint -p tsconfig.json -c tslint.json",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },

Build the Docker image. Use the newly-added script to build your Docker image. The first time will take a little bit longer while it pulls down the base image. After that, this step will go quite a bit faster.

npm run build:docker

Before running the Docker image, note that the npm start:docker script looks like this:

docker rm -f configtut; docker run --rm --name configtut -it -p 3000:3000 configtut /sbin/my_init -- bash -l

Note: When entering Docker commands at the command line, generally you'll want to enter them all on one line. In the documentation, they may sometimes wrap onto multiple lines. In the source code, they are written as single-line commands that you can not break across multiple lines. If you need to add line breaks, add a space, followed by the \ character at the end of the breaking line

The npm run build:docker command does the following:

  • Remove any previous image/container named configtut from prior runs
  • Run your image in a container named configtut
  • Run the container interactively so you can actually issue commands at a command prompt inside the container
  • Map the app server port (at 3000) to the host port at 3000. In other words, once you start the app, it's available on your local host at port 3000.

One final note: once the app starts up (or if it fails), you need to be aware of several things:

  • If the app fails, this container's init system will automatically attempt to restart it. You may notice looping errors in this case.
  • To exit from the container, you can type exit in the terminal where you started the container. This exits your bash session and the container shuts down. You can/should do this even if the container is scrolling output in your terminal window.
  • If this fails, or is confusing, you can stop the container at (a different) terminal prompt with docker rm -f configtut

Run the Dockerized app:

npm run start:docker

Note that when running the app the first time with the existing setup, it fails looking for development.env. We can make the setup generic and able to run in multiple active environments without requiring any source code changes by switching to the environment based mode, using the useEnv method.

Change the ConfigModule as shown below:

// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigManagerModule } from '@nestjsplus/config';

@Global()
@Module({
  imports: [
    ConfigManagerModule.register({
      // Comment out (or remove) useFile line
      // and add the useEnv line
      // useFile: 'config/development.env',
      useEnv: true,
      allowExtras: true,
    }),
  ],
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

Rebuild and re-run the Docker image. (Notice how much faster it builds the second time, since the constituent images have already been downloaded and cached).

npm run build:docker

pm run start:docker

The app should be up and running in your terminal window.

You can now browse to localhost:3000. Note the response in your browser window should now use the production DB_USERNAME ("myProdUser"). Note the logging information. You should see something like:

{
  DB_HOST: {
    dotenv: '--',
    env: '--',
    default: 'localhost',
    resolvedFrom: 'default',
    isExtra: false,
    resolvedValue: 'localhost'
  },
  DB_PORT: {
    dotenv: '--',
    env: '--',
    default: 5432,
    resolvedFrom: 'default',
    isExtra: false,
    resolvedValue: 5432
  },
  DB_USERNAME: {
    dotenv: 'myProdUser',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'myProdUser'
  },
  DB_PASSWORD: {
    dotenv: 'myProdPassword',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'myProdPassword'
  },
  DB_NAME: {
    dotenv: 'myProdDb',
    env: '--',
    default: '--',
    resolvedFrom: 'dotenv',
    isExtra: false,
    resolvedValue: 'myProdDb'
  }
}

Test that the app still runs locally (using the modified ConfigService). Stop the Docker app by typing exit in the terminal window.

Now start the app in the development environment with

npm run start:dev

The same configuration now recognizes the active environment has switched to development, and pulls in the development.env environment variables.

Test additional Docker support features

Let's do one more iteration. Let's assume that we do not want to have a .env file in production. Instead, we'll supply all the environment variables via Docker environment variables. This is the recommended configuration for production apps. We'll make one more change to the NestJSConfigManager setup to handle this.

Edit the config.module.ts file to look like this. We're adding the allowMissingEnvFile option to let NestJSConfigManager know that a missing .env file is not an error:

// src/config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigManagerModule } from '@nestjsplus/config';

@Global()
@Module({
  imports: [
    ConfigManagerModule.register({
      useEnv: true,
      allowExtras: false,
      allowMissingEnvFile: true,
    }),
  ],
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

And edit the Dockerfile so that it does not copy the production.env file into the Docker image:

# Dockerfile for NestjsConfigManager tutorial
######################################################
# BUILD
######################################################
FROM  unvjohn/nestjs-config:latest
LABEL MAINTAINER="johnfbiundo@gmail.com"

# Env Vars
# SRV_ROOT: root folder for the app module
# SRV_PORT: container will listen on SRV_PORT
#           map this to a host port in compose file or docker run command
#           (e.g., -p <host:container>)
ENV SRV_ROOT='/server' \
  SRV_PORT=3000

# Turn on NestjsConfigManager debugging
ENV DEBUG=cfg

WORKDIR $SRV_ROOT

# Remove the typescript starter Nest app from dist.  This was just there so
# we could test the base image.
RUN rm -r dist
RUN mkdir dist

# Install libs
COPY package.json .
RUN npm install

# Copy our app to dist
COPY dist dist

# Copy our production env file
# Comment out the following line to disable copying the production.env
# file into the Docker image
# COPY ./config/production.env ./config/production.env

# Listen on SRV_PORT
EXPOSE $SRV_PORT

Rebuild and re-run the Docker image This time, run it using the start:docker-testenv script. This script supplies the environment variables using docker -e options. Notice this part of the script:

-e 'DB_USERNAME=realProdUser' -e 'DB_PASSWORD=realProdPassword' -e 'DB_NAME=realProdDb'

There are a variety of ways of supplying environment variables to Docker. We use this one as a convenient way to test with the docker run command. Other Docker deployment tools like stacks and swarms have convenient mechanisms for managing Docker environment variables in production.

npm run build:docker

npm run start:docker-testenv

Browse again to localhost:3000 and notice that the app is now using the environment variables supplied via the Docker -e options on the command line.

You should notice in the log file some lines like those shown below. Notice the line about No env file found., which shows that we are reading all environment variables from the external environment.

  cfg > environment (using NODE_ENV): production +3ms
  cfg > envRoot:  /server +0ms
  cfg > resolving envfile path... +0ms
  cfg > ... using environment +0ms
  cfg > Parsing dotenv config file:  /server/config/production.env +1ms
  cfg > No env file found.  All env vars will come from external environment, if defined. +0ms
  cfg > Loaded 5 configuration spec keys. +1ms
  cfg > updatedConfig (after cascade):  {

This concludes the tutorial. Hopefully you've seen how you can use the NestJSConfigManager to easily and flexibly manage configuration variables across development, test and production environments, including containers like Docker.

Clone this wiki locally