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!
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! š
Top comments (20)
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".
Yeah cool solution! Yes, I also discovered proxy solution but didn't want to make it more complicated to readers. Thank you!
I hear that. You taught me a lot about Docker.
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.
Also true. Tiny American flags for some and ham sandwiches for others. We're transitioning to Docker at work so it was good info.
Good work though. Not trying to be one of "those" peopleš¤
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. :)
Thank you! Yeah post would be correct way to create user, also I made it to avoid using postman or curl in previous article.
Ahh yes, you can just CURL from the command line ;)
curl -d "foo=bar" http://localhost:8080/users
Is live reload works in case when uses port 3000 and I'm changing files locally ?
I think, it should not work. Because we didnt map our local files with files in docker. We need to make volumes for that.
A post explaining a bit more about Docker volumes would be welcome too. :)
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
Hi Andrei, for live updates, I changed my 'ui' service in my docker-compose to:
ui:
build: ./ui
ports:
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.
Have you found out any solution? I have been stucked in same situation.
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
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.
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?
I had to skip to the finished version because too much complication for a new user like me to troubleshoot the issues during build.
The finished version at bottom of post worked well.
One tip for other new users - when starting with docker-compose up --build while you have MongoDB running on local machine on same port, everything will work in the container except clicking create user will not save in the database. So it's a silent fail which is probably intentional. To test you can just stop local service of MongoDB, start docker and any user created is saved to DB which you can peruse with MongoDB Compass if you wish.
As a learning tutorial it's been great so thankyou very much!
Some comments have been hidden by the post's author - find out more