skip to Main Content

I am completely new to Docker & NGINX so I’m essentially learning as I go. I’m trying to get my Dockerized MERN app setup for production (not a critical app, just for my learning purposes), and after reading countless tutorials I’ve gotten so far as getting my app only for a Dev environment. However as the title suggests, I want to get this working for a prod-type environment (thinking of hosting in AWS), so I need to optimize it a bit more.

I’ll show you my Dockerfile.prod for the Frontend:

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build


FROM nginx
# Copy build assets from build/ folder & place them in nginx/html
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80

My production NGINX configuration file:

upstream client { 
    server client:3000;
}
upstream api {
    server api:3001;
}

server {
    listen 80;
    
    location / { 
        root /usr/share/nginx/html;
        include /etc/nginx/mime.types;
        try_files $uri $uri/ /index.html;
    }
    
    location /sockjs-node {
        proxy_pass http://client;
        proxy_http_version 1.1;
        # setting 'Upgrade' allows a tunnel setup between client & proxied server http://nginx.org/en/docs/http/websocket.html
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    location /api {
        proxy_pass http://api;
    }
}

My Dockerfile.prod for NGINX

FROM nginx
COPY ./prod.conf /etc/nginx/conf.d/default.conf

Lastly, my docker-compose.prod.yml file:

version: '3'
services:
  nginx:
    depends_on:
      - api
      - client
    restart: always
    build:
      dockerfile: Dockerfile.prod
      context: ./nginx
    ports:
      - "3000:80"
  api:
    build:
      dockerfile: Dockerfile
      context: ./backend
    volumes:
      - '/app/node_modules'
      - './backend:/app'
    command: npm start
  client:
    build: 
      dockerfile: Dockerfile.prod
      context: ./frontend
    volumes:
      - '/app/node_modules'
      - './frontend:/app'
    environment:
      - WDS_SOCKET_PORT=0
    tty: true
    stdin_open: true


From my understanding, the NGINX container is built first using its Dockerfile.prod, thus overriding its default.conf file with the prod.conf I made. Then upon the build of the ‘client’ container (my frontend) & the execution of its respective Dockerfile.prod, the build contents will be copied over to that NGINX’s html folder. So technically, that ‘Welcome to NGINX’ screen should’ve been overridden right?

However when inspecting the location where I copied my build contents, that doesn’t seem to be the case (see NGINX Image /usr/share/nginx/html contents). For what its worth, the client container seems to be built before the NGINX container does, at least from the order I see the logs being printed out (see Docker Compose Output). Regardless, it seems like I’m doing something wrong, and my only hunch is that the NGINX portion of the Frontend’s Dockerfile.prod is overriden by the execution of the NGINX’s Dockerfile.prod. Can anyone please help me? I’m just beyond confused at this point.

Directory Structure:

.
├── frontend
│   ├── Dockerfile
│   ├── Dockerfile.prod
│   ├── babel.config.json
│   ├── nginx
│   │   └── default.conf
├── nginx
│   ├── Dockerfile
│   ├── Dockerfile.prod
│   ├── default.conf
│   └── prod.conf
├── docker-compose.prod.yml
├── docker-compose.yml

Docker Compose Output

NGINX Image /usr/share/nginx/html contents

2

Answers


  1. Chosen as BEST ANSWER

    I want to preface that @Geilmaker's answer is the correct answer. However I also want to give additional details in case anyone has a similar setup.

    The main issue was as Geilmaker pointed out, that I essentially had two nginx images (one containing the build contents for the React portion). The first created during the Dockerfile.prod for the Frontend & the other created during the docker-compose.prod.yml execution:

     nginx:
       depends_on:
         - api
         - client
       restart: always
       build:
         dockerfile: Dockerfile.prod
         context: ./nginx
       ports:
         - "3000:80"
    

    But I didn't expose the nginx image with build contents. Rather, I exposed the one with just its default configuration. Furthermore, since the Dockerfile.prod for the Frontend only ran 'npm run build', the React app was not being served & prod.conf was configured to just default to its index.html for any default location - which at this point is only the welcome page.

    So what I had to do was to rely on the nginx proxy created for Dockerfile.prod for the Frontend & add some of the configuration used for the prod.conf into the default.conf that resides within the Frontend folder (for example the proxying for the Express api).

    The result looking like:

    frontend/Dockerfile.prod

    FROM node:18-alpine AS build
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build
    
    
    FROM nginx
    # Copy build assets from build/ folder & place them in nginx/html
    COPY --from=build /app/build /usr/share/nginx/html
    COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
    EXPOSE 80
    

    frontend/nginx/default.conf

    upstream client { 
        server client:3000;
    }
    upstream api {
        server api:3001;
    }
    
    server {
    
        listen 80;
        
        location / { 
            root /usr/share/nginx/html;
            include /etc/nginx/mime.types;
            index index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
    
        location /sockjs-node {
            proxy_pass http://client;
            proxy_http_version 1.1;
            # setting 'Upgrade' allows a tunnel setup between client & proxied server http://nginx.org/en/docs/http/websocket.html
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }
    
        location /api {
            proxy_pass http://api;
        }
    }
    

    And docker-compose.prod.yml looking like:

    version: '3'
    services:
      api:
        build:
          dockerfile: Dockerfile
          context: ./backend
        volumes:
          - '/app/node_modules'
          - './backend:/app'
        command: npm start
      client:
        build: 
          dockerfile: Dockerfile.prod
          context: ./frontend
        volumes:
          - '/app/node_modules'
          - './frontend:/app'
        environment:
          - WDS_SOCKET_PORT=0
        ports:
          - '3000:80'
        tty: true
        stdin_open: true
    
    

    Notice the removal of the 'nginx' definition & the port-mapping occuring within the 'client' definition.

    Upon executing

    docker-compose -f docker-compose.prod.yml up

    I can finally see my React application. Thanks @Geilmaker again 🙏


  2. Let’s start with serving the react app for production, I would recommend the following tutorial on this: https://dev.to/thexdev/dockerize-production-reactjs-3ai9

    So you might want to have the following Dockerfile.prod:

    FROM node:18-alpine AS build
    WORKDIR /app
    COPY package*.json ./
    RUN npm install
    COPY . .
    RUN npm run build
    
    FROM nginx:stable
    EXPOSE 80
    COPY ./your/path/default.conf /etc/nginx/conf.d/default.conf # That file needs to be created, you can adjust the first path to yours
    COPY --from=build /app/dist /usr/share/nginx/html  # Please have a look if its /app/dist or /app/build
    

    The default.conf file you need to create should look like the following:

    server {
        listen 80;
        root /usr/share/nginx/html;
        index index.html;
    
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
    

    I would also recommend creating a .dockerignore file in the same directory where your Dockerfile lives with the following content:

    node_modules
    

    That way you don’t copy that folder into the image – it’s created itself by the npm i command.

    With that done you should have a production react app.

    Then you should edit your docker-compose.prod.yml:

    version: '3'
    services:
      nginx:
        image: nginx:stable
        container_name: nginx
        depends_on:
          - api
          - client
        restart: always
        volumes:
          - ./sites-enabled:/etc/nginx/conf.d
        ports:
          - "3000:80"
      api:
        container_name: api
        build:
          dockerfile: Dockerfile
          context: ./backend
        volumes:
          - '/app/node_modules'
          - './backend:/app'
        command: npm start
        expose:
          - 3001
      client:
        container_name: client
        build: 
          dockerfile: Dockerfile.prod
          context: ./frontend
        expose:
          - 80
    

    That way you have an nginx instance, that should work as a proxy and route your requests to the api or client container.

    For that to work you need to create the sites-enabled directory where your docker-compose.prod.yml file lives (so it gets mounted as volume inside the nginx container). Inside that folder you need to create a <anyname>.conf file with the following content:

    server {
        listen 80;
        
        location / {
            proxy_pass http://client:80;
        }
    
        location /api/ {
            proxy_pass http://api:3001$request_uri;
        }
    }
    

    That would be a production setup. You have one image for each service and an nginx container, that is receiving the requests and route them to the corresponding service. You shouldn’t use the nginx instance inside your client container to also route requests to the api container, that would be bad practice. The nginx in your client image is only for making this container a standalone, it should not interact externally.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search