Containerizing your full-stack node app using Docker Compose
Tags: node, react, docker, devops
Published: 2023-11-04 10:28:13



Containerization, particularly with Docker, has revolutionized the way we develop, test, and deploy software. Docker Compose, an essential companion to Docker, simplifies the process of managing multi-container applications. In this article, we embark on a journey into the world of containerization, focusing on containerizing a full-stack Node.js application using Docker Compose. 

Whether you're a seasoned developer or just stepping into the world of containerization, this article will guide you through the process of Dockerizing a comprehensive application, from setting up your development environment to orchestrating multi-container setups effortlessly. By the end of this journey, you'll have a deep understanding of Docker Compose and the confidence to containerize your own full-stack Node.js applications, paving the way for efficient, scalable, and reliable software deployment. So, let's dive in and explore the transformative power of containerization in modern software development.

### **Prerequisite:**

This article assumes that you have:

- Docker Desktop
- Node and Npm
- Basic knowledge of react
- Basic knowledge of tailwind css

To follow along, you will need the following already installed on your computer:

- Docker Desktop
- Node js
- And a code editor

You will need tailwind CSS to get the styles I have used in this tutorial. Visit the [Tailwind CSS](https://tailwindcss.com/docs/guides/vite) documentation to get Tailwind up and running.  

Let’s create a folder for our project.
Open the command line on Windows and terminal on Mac, navigate to your desktop or your desired directory, and create a folder with 

`mkdir Docker_Compose`

Inside this folder 

navigate inside the folder you just created with 

`cd Docker_Compose`

inside the Docker_Compose folder, create two folders named frontend and backend with

`mkdir frontend` and  `mkdir backend`

## **Getting our backend ready**

navigate inside the backend folder with

`cd backend`

Initialize your express js backend package.json file with
`npm init -y`

The above command will create a package.json file in the root of your server and this file contains details about your project and its dependencies

Install Express.js using the following command.

`npm install express`

This will download and install the Express.js package for your project.

Before we get into creating our backend code, let’s create a script that starts up our server. In the root of our backend folder, open the package.json file and paste this command under scripts

`"start": "node server.js",`

Next, to avoid,cross-origin request issues from our backend, we’ll install cors npm package by running the following command

`npm i cors`

Open the Docker_Compose folder in your text editor and navigate to the backend folder you created.

Inside the backend folder, create a file called server.js, and paste the following code:

```jsx
const express = require("express");
const cors = require('cors')
const app = express();
const port = 8000;

app.use(cors())

app.get("/", (req, res) => {
  res.json([
    {
      id: 1,
      joke: "Why don't scientists trust atoms? Because they make up everything!",
    },
    { id: 2, joke: "I'm on a seafood diet. I see food and I eat it" },
    { id: 3, joke: "I used to be a baker because I kneaded dough" },
    {
      id: 4,
      joke: "Why don’t some couples go to the gym? Because some relationships don’t work out!",
    },
    {
      id: 5,
      joke: "I'm reading a book on anti-gravity. It's impossible to put down!",
    },
    {
      id: 6,
      joke: "Parallel lines have so much in common. It’s a shame they’ll never meet",
    },
    {
      id: 7,
      joke: "Why did the scarecrow win an award? Because he was outstanding in his field!",
    },
    {
      id: 8,
      joke: "Why don't oysters donate to charity? Because they are shellfish!",
    },
    { id: 9, joke: "I'm on a whiskey diet. I've lost three days already" },
    { id: 10, joke: "I used to play piano by ear, but now I use my hands" },
  ]);
});

app.listen(port, () => console.log(`Listening on port ${port}`));
```

Start your Express server using the following command:

`npm start`

The above command will run your server on port 8000 on localhost, which you can access on [http://localhost:8000](http://localhost:3000) in your browser.

## **Getting our frontend ready**

On our front end, we will be using Vite to initialize a React application.

Navigate to the frontend folder and input the following command

`npm create vite@latest`

After you are done initializing the react app, delete the content of the App.js file and paste the following code 

```jsx
import { useState, useEffect } from "react";

function App() {
  const [error, setError] = useState(false);
  const [jokes, setJokes] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("http://localhost:8000");
        if (response.ok) {
          const result = await response.json();
          setJokes(result);
          setError(true);
        } else {
          console.error("Error fetching data:", response.statusText);
        }
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };
    fetchData();
  }, []);

  return (
    <>
      <nav className='bg-red-50 py-4'>
        <p className='font-light text-4xl text-red-400  px-20 '>jokesVille</p>
      </nav>
      <main className='flex justify-center items-center px-6 py-10 bg-red-100'>
        {!error ? (
          <div
            className={`${
              error ? "flex" : "hidden"
            } h-full justify-center items-center text-center`}
          >
            <p className='text-xl font-medium'>
              So Sorry we could not find you jokes
              <span role='img' aria-label='dissapointed' className='text-4xl'>
                &#128542;
              </span>
            </p>
          </div>
        ) : (
          <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 justify-center items-center gap-x-4 gap-y-3 text-center my-10 py-5'>
            {jokes.map((joke) => (
              <div
                key={joke.id}
                className=' h-40 flex flex-col justify-center items-center bg-gray-50 rounded-md py-16 px-20'
              >
                <span role='img' aria-label='laugh'>
                  &#128514;
                </span>
                <p className='text-gray-500'>{joke.joke}</p>
              </div>
            ))}
          </div>
        )}
      </main>
    </>
  );
}

export default App;
```

Now fire up the jokesVille app frontend by running 

`npm run dev`

There we have our app running nice and shiny in our browser

![Frontend view in chrome](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20cjf77hczlvv76k9hf6.png)

Frontend view in chrome

To reap the benefits of docker we will need to go beyond directly running our app on the browser but through docker containers.

## Then comes Docker:

### What is Docker?

Docker is an open-source platform that enables developers to automate the deployment of applications in lightweight, portable containers. These containers are a form of virtualization that packages an application and its dependencies together, creating a consistent and isolated environment for running software.

### Why do we need docker?

1 **Consistency Across Environments:**

Developers often face the challenge of "it works on my machine" issues when deploying applications to different environments. Docker containers encapsulate applications and their dependencies, ensuring consistency across development, testing, and production environments. This consistency eliminates discrepancies between environments, leading to more reliable deployments.

2 **Isolation and Resource Efficiency:**

Docker containers offer both process and file system separation, enabling applications to operate autonomously from the host system. This separation guarantees that applications operate in their own space, preventing any interference and creating a stable, secure environment. Moreover, Docker's lightweight design demands fewer resources in comparison to conventional virtual machines, enhancing efficiency in terms of both memory and processing capabilities.

3 **Microservices Architecture:**

In contemporary software development, the microservices architecture is extensively utilized. Docker simplifies the creation and supervision of microservices by enabling each service to operate within its dedicated container. This method of development, which emphasizes modularity, improves scalability, fault tolerance, and simplifies maintenance.

### What is Docker Compose?

Docker Compose is a utility designed to define and oversee complex Docker applications with multiple containers. It allows the comprehensive description of an application setup, encompassing services, networks, and storage allocations, within a unified docker-compose.yml file. This file serves as a plan for your application, outlining the construction, configuration, and interconnections of diverse containers.

To reap the above benefits, let’s get into running our app in Docker

To ensure our front-end displays in the browser when we run it in a container, go into vite.config file and replace the contents of the file with the following

```jsx
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
 base: "/",
 plugins: [react()],
 server: {
  port:3000,
  strictPort: true,
  host: true,
 },
});

```

You may choose to keep the default port: 5173, in my case I went with port: 3000. If you decide to go with a different port, do remember to change it accordingly in the frontend Dockerfile.

## **Containerizing our Application**

To start, we have to create 2 docker files,1 in our frontend folder and 1 in our backend folder.
First, navigate into the frontend folder and create a file named Dockerfile, then paste the following command into the file:

```docker
FROM node:20.9.0-alpine3.18
WORKDIR /Docker_Compose/frontend
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
```

Before we move on to creating our backend docker file, let’s take some time to explain the content of the above docker file and what they do:

`FROM node:20.9.0-alpine3.18`

This line defines the foundational image for the Docker container. It begins with a minimal Alpine Linux system (version 3.18) and then adds Node.js version 20.9.0 onto this base system.

`WORKDIR /Docker_Compose/frontend`

This sets the working directory within the container to `/Docker_Compose/frontend`.

`COPY packag.json ./`

This line copies `package.json` from the host machine to the current working directory in the container (`/Docker_Compose/frontend`).

`RUN npm install``

This command installs the Node.js dependencies specified in `package.json`

`COPY . .`

This command copies all files from the directory on the host machine where the Dockerfile is located to the active working directory within the container. This encompasses various items such as the application code and extra configuration files.

`EXPOSE 3000`

This informs Docker that the container will listen on port 3000 at runtime.

`CMD ["npm","run","dev"]`

This specifies the default command to run when the container starts. 

To create our backend docker file, navigate to the backend folder, create a file named Dockerfile, and paste in the following commands

```docker
FROM node:20.9.0-alpine3.18
WORKDIR /Docker_Compose/backend
COPY package.json .
RUN npm install
COPY . .
EXPOSE 8000 
CMD ["npm", "start"]
```

Next, navigate to the root folder (Docker_Compose) and create a file named docker-compose.yaml, then paste the following text

```yaml
version: "3.8"
services:
  frontend:
    build: ./frontend
    container_name: jokesVille_client
    ports:
      - "3000:3000"
  backend:
    build: ./backend
    container_name: jokesVille_server
    ports:
      - "8000:8000"
```

In the root folder  (Docker_Compose), and with Docker Desktop running, run the below command

`docker-compose up --build`

If you have been following the tutorial correctly, you should be greeted with a screen similar to this

![Docker Compose frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3zdf7yedthlice4bhljr.png)

Open http://localhost:8000/ and view the jokesville backend

![jokesville backend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tbbmhwimpnkgp74fbxqg.png)
Open [http://localhost:3000/](http://localhost:3000/) on your browser and you’ll have our jokesville frontend nicely displayed and connected to the backend.
![jokesville frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7it4sehpmmtrys16j5nd.png)

To shut down the docker containers, in the root folder run 

`docker-compose down`

## Conclusion

In this article, you've learned the process of containerizing your full-stack web application with Docker Compose. Initially, we set up a backend Express server with Node.js and developed a front-end application using React.js. Following these stages, Dockerfiles were generated for both applications, alongside a docker-compose.yaml file.
Using this docker-compose.yaml file, we built and launched the application containers.