-
Notifications
You must be signed in to change notification settings - Fork 6
Tutorial
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.
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' } }
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 aboutphusion/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 at3000
. In other words, once you start the app, it's available on your local host at port3000
.
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 yourbash
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.
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.