Configuring and Using .env files

Configuring and Using .env files

by John Vincent


Posted on May 24, 2021


The multitude of requirements create the requirement there be many .env files. This requirement creates some challenges.

This is part of a series of discussions regarding Deploying TaskMuncher, a React and Node application, to a Multi-Container Docker Environment at AWS using Dockerhub and Travis CI

For more details, please see Overview of Create Multi-Container Docker TaskMuncher Application at AWS

The following sections include

Overview

The traditional solution is to configure the application to get a .env file from the root of the application.

The .env file would be replaced in a production environment with a .env from private storage.

This simple approach is usually sufficient.

However, the requirements of this project far exceed the usual. Let's review.

Docker Requirements

Config Folder Architecture

Notice that client, a React application, requires 3 .env files and server, a Node/Express application requires 2 .env files.

Further, configuring a Docker image for development use often requires the source code be mounted into the container, for example docker-compose.yml

version: '3'
services:
  client:
    build: .
    ports:
      - 8080:8080
    volumes:
      - ./client:/app

...

which mounts my local ./client into the container at /app.

This will also copy .env if it is in the ./client directory or any sub-directory.

Further, it is not possible to copy another .env file to /app. The .env file has to copied elsewhere in the file system and must be referenced by the application.

Thus the traditional structure of providing a .env file in the root directory of the application cannot satisfy the requirements.

Non-Docker Requirements

The components client and server are also built individually in the customary manner.

Client Application

The client application is built with React. The requirement is to build an application for

  • Development
  • Production in a Development environment
  • AWS Production in a Docker environment
  • Non-AWS Production

which can be seen in the following partial package.json

		"start": "ENV_FILE=$(pwd)/envs/dev.env webpack serve",
		"build:dev:prod": "ENV_FILE=$(pwd)/envs/dev-prod.env     npm run build:actual:production",
		"build:production": "ENV_FILE=$(pwd)/envs/production.env npm run build:actual:production",
		"build:actual:production": "rm -rf dist && webpack --mode production --progress"

Server Application

The server application is built with Node/Express. The requirement is to build an application for

  • Development
  • Production in a Development environment
  • Production
  • Unit tests

which can be seen in the following partial package.json

		"nodemon": "ENV_FILE=./envs/dev.env nodemon server.js",
		"start": "ENV_FILE=./envs/dev.env node server.js",

		"start:dev:prod": "ENV_FILE=./envs/dev-prod.env node server.js",
		"start:production": "ENV_FILE=./envs/prod.env node server.js",

		"test": "ENV_FILE=./envs/unit-test.env jest ./test",

Solution

An example of the general solution is

"start": "ENV_FILE=$(pwd)/envs/dev.env webpack serve",
  • set ENV_FILE to the .env file
  • execute the application

Client - React

To obtain the ENV_FILE values, partial webpack.config.js follows

const { ENV_FILE } = process.env;
require('dotenv').config({ path: ENV_FILE });

const { HOME_HOST, HOME_PORT } = process.env;

config.devServer = {
		disableHostCheck: true,
		contentBase: DIST_FOLDER,
		compress: false,
		host: HOME_HOST,
		port: HOME_PORT,
		clientLogLevel: 'info',
		historyApiFallback: true,
		proxy: {
			'/api/**': {
				target: 'http://localhost:3001',
				changeOrigin: true,
				secure: false
			}
		}

which uses the dotenv package.

Client .env

For a non-Docker .env file

HOME_HOST=localhost
HOME_PORT=8040

HOME_URL=http://localhost:8040

SERVER_APIS_URL=http://localhost:3110

whereas a .env for Docker

HOME_HOST=0.0.0.0
HOME_PORT=8040

HOME_URL=http://localhost:8100
SERVER_APIS_URL=http://localhost:8100

For use in a Docker container

config.devServer = {

		host: HOME_HOST,
		port: HOME_PORT,

is required.

Server - Node/Express

An example of the solution

"nodemon": "ENV_FILE=./envs/dev.env nodemon server.js",
  • set ENV_FILE to the .env file
  • execute nodemon server.js

A partial ./server.js

const express = require('express');
const cors = require('cors');

const { CONFIG } = require('./config/config');

const { PORT, DATABASE_URL, WHITE_LIST } = CONFIG;

const whitelist = WHITE_LIST;
const corsOptions = {
	origin(origin, callback) {
		// console.log('origin ', origin);
		if (!origin || whitelist.indexOf(origin) !== -1) {
			callback(null, true);
		} else {
			callback(new Error('Not allowed by CORS'));
		}
	}
};
app.use(cors(corsOptions));

and a partial ./config/config.js

const { ENV_FILE } = process.env;
require('dotenv').config({ path: ENV_FILE });

const WHITE_LIST = process.env.WHITE_LIST.split(',');

const CONFIG = {
	PORT: 1 * process.env.PORT,
	HOME_URL: process.env.HOME_URL,

	WHITE_LIST,
	DATABASE_URL: process.env.DATABASE_URL
};

exports.CONFIG = CONFIG;

which can be referenced by

const { CONFIG } = require('./config');

Server .env

For a non-Docker .env file

HOME_URL=http://localhost:8040
PORT=3110
WHITE_LIST=http://localhost:8040

whereas a .env for Docker AWS Production

HOME_URL=http://taskmuncherdocker-env.eba-mv2hwnxx.us-east-1.elasticbeanstalk.com
PORT=3110
WHITE_LIST=http://taskmuncherdocker-env.eba-mv2hwnxx.us-east-1.elasticbeanstalk.com