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
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_FILEto the.envfile - 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_FILEto the.envfile - 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