loading...
Cover image for Dockerize a React app with Node.js backend connected to MongoDb

Dockerize a React app with Node.js backend connected to MongoDb

vguleaev profile image Vladislav Guleaev ใƒป5 min read

Hello dear coder, welcome to my last tech article of series dedicated to Node.js and Docker. Hope you enjoy!

Problem:

We already now how to use Docker together with Node and Mongo from previous article in this series. In order to complete our MERN stack application we need to add frontend part. In our case, frontend will be implemented using React. Let's learn how to create full working application with frontend, backend, database and run everything in Docker!

1. Clone backend Node.js

In previous part of this series we created a Node.js app using MongoDb with Docker. For this tutorial we will need the same project. Clone source code from here or run this command:

git clone https://github.com/vguleaev/Express-Mongo-Docker-tutorial.git

After cloning is done, rename folder from test-mongo-app to api. This will be our backend.

To test that everything works, open api folder and run npm install. After dependencies are installed, let's check if everything works. ๐Ÿพ

docker-compose up

This command will use our docker-compose.yml to pull mongo image and start express server connected to MongoDb.

If everything is ok you should see in console something like this:

web_1    | Listening on 8080
web_1    | MongoDb connected

Open in browser this endpoint http://localhost:8080/users and you should get an empty array as response. Which is correct because our database is completely empty for now.

2. Create React app

Time to develop our frontend part. Go up to parent directory and run:

npm i create-react-app -g
create-react-app ui

Right now our folder structure should look like this:
...
โ”œโ”€โ”€ / api
โ””โ”€โ”€ / ui
(Where api is cloned backend app and ui is newly created React app.)

To be sure that everything works let's open ui folder and start React app:

cd ui
npm start

You should see basic React app at http://localhost:3000. ๐ŸŽˆ

3. Dockerize React app

In ui folder create a .dockeringore file:

node_modules
.git
.gitignore

(Without this file, our docker build command will be just hanging on Windows.)

Also create a Dockerfile file in ui folder:

FROM node:8
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./

RUN npm install --silent
# Copy app source code
COPY . .

#Expose port and start application
EXPOSE 3000
CMD ["npm", "start"]

Let's test that React works in docker. First we will build the image with tag react:app:

docker build -t react:app .

Now run our tagged image and use the same port for docker:

docker run -p 3000:3000 react:app

Open http://localhost:3000 and you should see React served from Docker. ๐Ÿ‘

โš ๏ธ If you just close like you usually do with Ctrl+C container won't stop. To stop container from running do docker ps command.

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
06c982ce6ae9        react:app           "docker-entrypoint.sโ€ฆ"   12 days ago         Up About a minute   0.0.0.0:3000->3000/tcp   strange_montalcini

Then pick desired id and stop container.

docker stop 06c982ce6ae9

4. Call api from React app

Open ui folder and install axios

cd ui
npm i axios

We will change App component a bit to have a button to create users and show list of users ids. We will call /user-create and /users GET endpoints from our Nodejs app.

Paste this into App.js file:

import React, { Component } from 'react';
import logo from './logo.svg';
import axios from 'axios';
import './App.css';

const apiUrl = `http://localhost:8080`;

class App extends Component {
  state = {
    users: []
  };

  async createUser() {
    await axios.get(apiUrl + '/user-create');
    this.loadUsers();
  }

  async loadUsers() {
    const res = await axios.get(apiUrl + '/users');
    this.setState({
      users: res.data
    });
  }

  componentDidMount() {
    this.loadUsers();
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <button onClick={() => this.createUser()}>Create User</button>
          <p>Users list:</p>
          <ul>
            {this.state.users.map(user => (
              <li key={user._id}>id: {user._id}</li>
            ))}
          </ul>
        </header>
      </div>
    );
  }
}

export default App;

Since we run frontend on port 3000 but backend is running on port 8080 we are going to have a CORS problem. To avoid it go to api project and install cors package.

npm i cors

Then use it in server.js file:

const express = require('express');
const app = express();
const connectDb = require('./src/connection');
const User = require('./src/User.model');
const cors = require('cors');

app.use(cors());
// ...

5. Run React and Node together in Docker

Final step! Now remove docker-compose.yml from directory api and create docker-compose.yml in root folder. Paste this:

version: '2'
services:
  ui:
    build: ./ui
    ports:
      - '3000:3000'
    depends_on:
      - api
  api:
    build: ./api
    ports:
      - '8080:8080'
    depends_on:
      - mongo
  mongo:
    image: mongo
    ports:
      - '27017:27017'

Our root folder structure now looks like this:
...
โ”œโ”€โ”€ / api
โ”œโ”€โ”€ / ui
โ””โ”€โ”€ docker-compose.yml

We have one docker-compose that describes what services we want to run in Docker. In our case we have three services: ui, api, mongo. ๐Ÿ‹

For each service will be created docker image using Dockerfile in each project. We specify the path in line build. (e.g. build: ./ui)

For mongo we don't have project to build image, because we use predefined image from docker hub. (e.g. image: mongo)

We also specify ports and dependencies. In our case first will be started mongo on port 27017, because api depends on mongo. Second container is api on port 8080 because ui depends on it. Last container is ui which starts on port 3000.

Finally from root folder run all services with one command! ๐Ÿง™

docker-compose up --build

Open http://localhost:3000/ and click on button to create users. Open Developer tools to have a look at calls. Now we run both frontend and backend from docker!

Alt Text

6. Use React production build

Right now we start our React app with development server which is probably not what we want to use in production. But we can easy fix this problem.

We simply need to change our Dockerfile in ui project. We will start a production build and serve it using nginx server. Replace everything with this:

# build environment
FROM node:12.2.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json /app/package.json
RUN npm install --silent
RUN npm install react-scripts@3.0.1 -g --silent
COPY . /app
RUN npm run build

# production environment
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Since we now expose port 80, we need to change it from 3000 to 80 in docker-compose.yml.

  ui:
    build: ./ui
    ports:
      - '80:80'
    depends_on:
      - api

Now run again magic command to start everything in docker ๐Ÿ”ฎ

docker-compose up --build

Open http://localhost/ and you should see exactly the same working application but now React is running in production mode.

See the source code here. Enjoy!

Congratulation you successfully dockerized React, Nodejs and Mongodb! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰


๐Ÿš€ If you read something interesting from that article, please like and follow me for more posts. Thank you dear coder! ๐Ÿ˜

Discussion

pic
Editor guide
Collapse
xybolx profile image
Mat Hayward

In the package.json for "ui" you could add a proxy and avoid the CORS issue without the CORS package. "proxy": localhost:8080,
Just remember to delete it before you run "build".

Collapse
vguleaev profile image
Vladislav Guleaev Author

Yeah cool solution! Yes, I also discovered proxy solution but didn't want to make it more complicated to readers. Thank you!

Collapse
xybolx profile image
Mat Hayward

I hear that. You taught me a lot about Docker.

Thread Thread
misterhtmlcss profile image
Roger K.

Haha Matt I was thinking the same thing. I think it needed to be said and I think Vlad's reasoning is solid. Compromises are always necessary in a shirt piece, but saying that it's still good to know alternative patterns.

Thread Thread
xybolx profile image
Mat Hayward

Also true. Tiny American flags for some and ham sandwiches for others. We're transitioning to Docker at work so it was good info.

Collapse
xybolx profile image
Mat Hayward

Good work though. Not trying to be one of "those" people๐Ÿค

Collapse
akarabach profile image
Andrei Karabach

Is live reload works in case when uses port 3000 and I'm changing files locally ?

Collapse
vguleaev profile image
Vladislav Guleaev Author

I think, it should not work. Because we didnt map our local files with files in docker. We need to make volumes for that.

Collapse
stanleysathler profile image
Stanley Sathler

A post explaining a bit more about Docker volumes would be welcome too. :)

Thread Thread
workmap profile image
Tyler McCracken

Hi Stanley, for live updates, I changed my 'ui' service in my docker-compose to:
ui:
build: ./ui
ports:
- '3000:3000'
depends_on:
- api
volumes:
- ./ui/src/:/usr/src/app/src
stdin_open: true

Collapse
workmap profile image
Tyler McCracken

Hi Andrei, for live updates, I changed my 'ui' service in my docker-compose to:
ui:
build: ./ui
ports:

  • '3000:3000' depends_on:
  • api volumes:
  • ./ui/src/:/usr/src/app/src stdin_open: true
Collapse
glenhughes profile image
Glen Hughes

Great article. I enjoyed reading it. I think this article gives a good top level explanation on how to get this working.

Only change I would make is use a post rather then a get to create the user. :)

Collapse
vguleaev profile image
Vladislav Guleaev Author

Thank you! Yeah post would be correct way to create user, also I made it to avoid using postman or curl in previous article.

Collapse
glenhughes profile image
Glen Hughes

Ahh yes, you can just CURL from the command line ;)
curl -d "foo=bar" http://localhost:8080/users

Collapse
emilkloeden profile image
Emil Kloeden

Hi, this series is fantastic, thank you. I built something heavily influenced by this but ran into cors issues when running the production deployment and trying to access it via my host machines IP address. Is there some low hanging fruit I should try? I have tried changing my react app to fetch data from localhost:3001 to api:3001 but that is also unsuccessful. Code is here if anyone has time to look :) github.com/emilkloeden/covid.

Collapse
bhuwanadhikari profile image
Bhuwan Adhikari

Have you found out any solution? I have been stucked in same situation.

Collapse
ntzamos profile image
Nikos Tzamos

Thank you for your article, you help us a lot.
I wonder if instead of nginx we can serve the ui from nodejs via Express and have it all in one Docker once more.

Also, if we don't use any other interface with the api, could the api be replaced by socket.io to directly connect the react ui with nodejs.

I hope I make any sense, as I am thinking of deploying small CRUD apps with MERN stack.

Cheers

Collapse
misterhtmlcss profile image
Roger K.

You use Nginx because it acts as a proxy between the internet and your back end if I recall the reasoning. So it's a security issue and that's why most projects with this stack typically involve Nginx.

Collapse
kostaslamo profile image
Konstantinos Lamogiannis

Great article cheers! Just a little note/question. Deploying this at a remote server, having react served by Nginx in clients browser, API requests to localhost will fail. That's because the browser in every client will try to make a request to a localhost:8080 server. So i assume, the production build should also have a functionality of getting the IP of the host that these 2 containers will run in the production server. How would you approach this situation?

Some comments have been hidden by the post's author - find out more