'Production vs Development Docker setup for Node (Express & Mongo) App

I'm attempting to convert a Node app to using Docker but running into a few issues/questions I'm unable to answer.

But for simplicity I've included some very basic example files to keep the question on target. In fact the example below merely links to a Mongo container but doesn't use it in the code to keep it even simpler.

Primarily, what Dockerfile and docker-compose.yml setup is required to successfully use Docker on a Node + Express + Mongo app on both local (OS X) development and for Production builds?

Dockerfile

FROM node:6.3.0

# Create new user to avoid using root - is this correct practise?
RUN useradd --user-group --create-home --shell /bin/false app

COPY package.json /home/app/code/
RUN chown -R app:app /home/app/*

USER app
WORKDIR /home/app/code
# Should this even be set here or use docker-compose instead?
# And should there be:
#  - docker-compose.yml setting it to production by default
#  - docker-compose.dev.yml setting it to production?
# Or reverse it? (docker-compose.prod.yml instead with default being development?)
# Commenting below out or it will always run as production
#ENV NODE_ENV production
RUN npm install

USER root
COPY . /home/app/code
# Running chown to ensure new 'app' user owns files
RUN chown -R app:app /home/app/*
USER app

EXPOSE 3000

# What CMD should be here to ensure development versus production is simple?
# Development - Restart server and refresh browser on file changes
# Production  - Ensure uptime. 
CMD ["npm", "start"]

docker-compose.yml

version: "2"
services:
  web:
    build: .
    # I would normally use a .env file but for this example will set explicitly
    # env_file: .env
    environment:
      - NODE_ENV=production
    volumes:
      - ./:/home/app/code
      - /home/app/code/node_modules
    ports:
      - "3000:3000"
    links:
      - mongo
  mongo:
    image: mongo
    ports:
      - "27017:27017"

docker-compose.dev.yml

version: "2"
services:
  web:
    # I would normally use a .env file but for this example will set explicitly
    # env_file: .env
    environment:
      - NODE_ENV=development

package.json

{
  "name": "docker-node-test",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.14.0",
    "mongoose": "^4.6.1",
    "nodemon": "^1.10.2"
  },
  "devDependencies": {
    "mocha": "^3.0.2"
  }
}

1. How to handle the different NODE_ENV (dev, production, staging)?

This is my primary question and conundrum.

In the example I’ve used the NODE_ENV is set in the Dockerfile as production and there are two docker-compose files:

  • docker-compose.yml sets the defaults include NODE_ENV to production
  • docker-compose.dev.yml overrides the NODE_ENV and sets it to development

1.1. Is it advised to rather switch that order around and have development settings as the default and instead use a docker-compose.prod.yml for overrides?

1.2. How do you handle the node_modules directory?

I'm really not sure how to handle the node_modules directory at all between local development needs and then running for Production. (Perhaps I have a fundamental misunderstanding though?)


Edit:

I added a .dockerignore file and included the node_modules directory as a line. This ensures the node_modules dir is ignored during the copy, etc.

I then edited the docker-compose.yml to include the node_modules as a volume.

volumes:
  - ./:/home/app/code
  - /home/app/code/node_modules

I have also put the above change into the full docker-compose.yml at the start of the question for completeness.

Is this even a solution?

Doing the above ensured I could have my local development npm install included dev-dependencies. And when running docker-compose up it pulls in the production only node modules inside the Docker container (since the default docker-compose.yml is set to NODE_ENV=production).

But it seems the NODE_ENV set inside the 2 docker-compose files aren't taken into account when running docker-compose -f docker-compose.yml build :/ I expected it to send NODE_ENV=production but ALL of the node_modules are re-installed (including the dev-dependencies).

Do we instead use 2 Dockerfiles? (Dockerfile for Prod; Dockerfile.dev for local development)

(I feel like that is a fundamental piece of logic/knowledge I am missing in the setup)


2. Nodemon vs PM2

How would one use nodemon on the local development machine but PM2 on the Production build?

3. Should you create a user inside the docker containers and then set that user to be used in the Dockerfile?

It uses root user by default but I’ve not seen many articles talking about creating a dedicated user within the container. Am I correct in what I’ve done for security? I certainly wouldn’t feel comfortable running an app as root on a non-Docker build.

Thank you for reading. Any and all assistance appreciated :)



Solution 1:[1]

    1. Either, it doesn't matter too much, I prefer to have development details then overwrite with production details.

    2. I don't commit them to my repo, then I have "npm install" in my dockerfile.

  1. You can set rules in the dockerfile to which one to build based on build settings.

  2. It is typical to build everything via root, and run the main program via root. You can set up other users, but for most uses it is not needed as the idea of docker containers is to isolate each process in individual docker containers.

Solution 2:[2]

I can share my experience, not saying it is the best solution.

  1. I have Dockerfile and dockerfile.dev. In dockerfile.dev I install nodemon and run the app with nodemon, the NODE_ENV doesn't seem to have any impact. As for users you should not use root for security reasons. My dev version:

     FROM node:16.14.0-alpine3.15
    
     ENV NODE_ENV=development
    
     # install missing libs and python3
     RUN apk update && apk add -U unzip zip curl && rm -rf 
     /var/cache/apk/* && npm i [email protected] [email protected] -g
    
     WORKDIR /node
    
     COPY package.json package-lock.json ./
    
    
     RUN mkdir /app && chown -R node:node .
     USER node
    
     RUN npm install && npm cache clean --force
    
     WORKDIR /node/app
    
     COPY --chown=node:node . .
    
    # local development
     CMD ["nodemon", "server.js" ]
    

in Production I run the app with node:

FROM node:16.14.0-alpine

ENV NODE_ENV=production

# install missing libs and python3
RUN apk update && apk add -U unzip zip curl && rm -rf /var/cache/apk/* \
&& npm i [email protected] -g

WORKDIR /node

COPY package.json package-lock.json ./

RUN mkdir /app && chown -R node:node .
USER node

RUN npm install && npm cache clean --force

WORKDIR /node/app

COPY --chown=node:node . .

CMD ["node", "server.js" ]

I have two separate versions of docker-compose. In docker-compose.dev.yml I set the dockerfile to dockerfile.dev:

app:
    depends_on:
      mongodb:
        condition: service_healthy
    build:
      context: .
      dockerfile: Dockerfile.dev
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:5000" ]
      interval: 180s
      timeout: 10s
      retries: 5
    restart: always
    env_file: ./.env
    ports:
      - "5000:5000"
    environment:
       ...
    volumes:
      - /node/app/node_modules

In production docker-compose.yml there is the dockerfile set to Dockerfile.

  1. Nodemon vs PM2. I used pm2 before dockerizing the app. I cannot see any benefit of having it in docker, the restart: always takes care about restarting on error. You should better use restart: unless_stopped but I prefer the always option. Initially I used nodemon also on production so that the app reflected the volumes changes but I skipped this because the restart didn't work well (it was waiting for some code changes..).

  2. Users: You can see it in my example. I took a course for docker + nodejs and setting a non-root user was recommended so I do it and I have no problems.

I hope I explained well enough and it can help you. Good luck.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 AndrewL
Solution 2 Yoshi