Dockerizing The MERN Stack (Without Docker Compose)
Tags: tutorial, webdev, devops, docker
Published: 2020-12-27 10:07:04

This is the fifth post in my [Demystifying Docker](https://dev.to/rinkiyakedad/series/10009) series. I'll be showing you how you can use Docker when working with the [MERN stack](https://www.mongodb.com/mern-stack). You will need to know the basics of [containers](https://dev.to/rinkiyakedad/learning-to-use-docker-298b), [volumes](https://dev.to/rinkiyakedad/diving-deeper-into-docker-volumes-and-bind-mounts-4fp0), and [networking](https://dev.to/rinkiyakedad/cross-container-communication-and-networking-in-docker-39n0) with [Docker](https://dev.to/rinkiyakedad/introduction-to-docker-1hp2) in order to understand this post. So if you're not aware of them I highly recommend you to go check the other posts out. With that out of the way let's get started.

I'll be showing you the Dockerfiles you'll need to write and the commands you'll be using to spin up your containers and explaining them step by step in the following order:
1. Database
2. Backend
3. Frontend

Do note that this will be a development setup with Docker and not a production setup because we will be configuring stuff in a way that provides us much needed development features like live updating of our app when changes are made to the code. I will be covering how to set up Docker for production and deployment in a future post. 

Let's begin!

### Initial Setup
Before we start with anything let us create a [network](https://dev.to/rinkiyakedad/cross-container-communication-and-networking-in-docker-39n0) that will help our containers interact with each other easily. 
```
docker network create mern-net
```

### Database
We won't be needing a Dockerfile for MongoDB since we'll be grabbing the official image from [Docker Hub](https://hub.docker.com/_/mongo). This official image also gives us two optional environment variables which we can pass to restrict access to our database. Those being `MONGO_INITDB_ROOT_USERNAME` and `MONGO_INITDB_ROOT_PASSWORD`.

Whenever some container would want to connect to this running MongoDB container it will have to use the username and password we specify here. Apart from that, we will also set up a [named volume](https://dev.to/rinkiyakedad/diving-deeper-into-docker-volumes-and-bind-mounts-4fp0) so that even if you remove this MongoDB container and start a new one, our previously stored data is not lost. Taking all this into consideration our final command for starting the container would be:

```
docker run --name mongodb -v data:/data/db --rm -d --network mern-net -e MONGO_INITDB_ROOT_USERNAME=arsh -e MONGO_INITDB_ROOT_PASSWORD=pass123 mongo
```

Here, 
1. `mongo` is the name of the image we grab from Docker Hub
2. `mongodb` is the name we give to this container
3. `/data/db` is the [path](https://hub.docker.com/_/mongo) where data is stored inside this container.
4. `--rm` flag tells Docker to remove the container if we stop it.
5. `-d` flag tells Docker to run the container in [detached mode](https://www.freecodecamp.org/news/docker-detached-mode-explained/). 

### Backend

For the backend we will need a Dockerfile which will look like this:
```
FROM node

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

ENV MONGODB_USERNAME=root
ENV MONGODB_PASSWORD=pass

CMD ["npm", "start"]
```

For our changes to be reflected automatically in the containerized backend we will also need to add `nodemon` as a dependency and create a `start` script. So the `package.json` would look something like this:

```
 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js"
  },
 "devDependencies": {
    "nodemon": "^2.0.4"
  }
```

Inside our code, we would also have to change the Docker connection string to specify the username and password from the environment variables. The connection string should look something like this:

```
mongodb://${process.env.MONGODB_USERNAME}:${process.env.MONGODB_PASSWORD}@mongodb:27017/mern-project?authSource=admin
```

Do remember to add the `?authSource=admin` at the end of the string if it's not present already.

And our final command to run the container would be:

```
docker run --name mern-backend -v /home/arsh/docker-examples/mern-app/backend:/app -v /app/node_modules -e MONGODB_USERNAME=arsh -e MONGODB_PASSWORD=pass123 -d --rm -p 80:80 --network mern-net mern-node:latest
```

Explanation:

1. We used a bind mount to make a link with our code inside the container so that the latest changes in code are picked up automatically. We also used an anonymous volume so that while using the bind mount our `node_modules` folder doesn't get lost. (explained in detail in the [third post](https://dev.to/rinkiyakedad/diving-deeper-into-docker-volumes-and-bind-mounts-4fp0))

2. We need to pass the environment variables `MONGODB_USERNAME` and `MONGODB_PASSWORD`, the default values for which we specified in the Dockerfile as `root` and `pass` respectively. These will be plugged into the MongoDB connection string present in our code.

3. We published the post 80 (on which our app runs) as this will be used by the frontend to interact with the backend. We won't be using our docker network for a connection between frontend and backed so we need to publish the port.

4. We need to use our database container name (`mongodb`) in the connection string and specify the `mern-network` while starting this container so that our backend can interact with the database.

5. `mern-node:latest` should be replaced with whatever you named your image while building it from the Dockerfile.


### Frontend

For the frontend our Dockerfile should look like this:

```
FROM node

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "npm", "start" ]
```

And we will run the following command to start this:

```
docker run -v /home/arsh/docker-examples/mern-app/frontend/src:/app/src --name mern-frontend --rm -p 3000:3000 -it mern-react
```

Explanation:
1. We use a bind mount so that changes in our code are reflected automatically in the running frontend container.

2. We publish the port 3000 and will use it to interact with our app from the browser. We don't add the frontend to our `mern-network` since the code for it gets executed in the browser. It wouldn't be possible to interact via the localhost if we add it to our docker network. And since it is not part of the docker network, we published the `port 80` while starting the backend container.

3. It is a requirement that the frontend container be started in [interactive mode](https://phoenixnap.com/kb/docker-run-command-with-examples#:~:text=Docker%20allows%20you%20to%20run,while%20it%20is%20still%20running.&text=The%20command%20prompt%20will%20change,as%20in%20the%20example%20below.), so we also use the `-it` flag. This has to do with the way `create-react-app` works.

4. `mern-react` can be replaced with whatever you named your image while building it from the Dockerfile.


### Conclusion
With this, we are done dockerizing our MERN app. Visit `https://localhost:3000` to interact with your functioning dockerized code!

Thanks for reading :D

If you have any feedback for me or just want to talk feel free to connect with me on [Twitter](https://twitter.com/RinkiyaKeDad). I'll be more than happy to help you out! :)