Adding Dockerfiles to Open-Source Projects
Tags: opensource, docker, devops, webdev
Published: 2023-11-24 03:14:40

This week, I continued my **open source journey** by making a meaningful contribution to yet another project.

My [last pull request](https://github.com/tarasglek/chatcraft.org/pull/271) was for a UI bug fix in [ChatCraft](https://chatcraft.org/) - a developer oriented ChatGPT clone. It was part of the [5 contributions](https://dev.to/amnish04/hacktober-2023-recap-4f6n) I made for [Hacktoberfest](https://hacktoberfest.com/about/) during the month of October.

![October contributions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lwn7lr93ti60qsm6jzdp.png)

Since I am currently taking a [cloud computing](https://apps.senecacollege.ca/ssos/findOutline.do?subjectCode=CCP555&_gl=1*tweh1p*_ga*MTM4MDc0OTUyLjE2NzkzNjQ4NjI.*_ga_NKW0CZJEK5*MTcwMDc4ODM2OS42MC4xLjE3MDA3ODgzODIuNDcuMC4w&_ga=2.253959552.909941902.1700788370-138074952.1679364862) course at school, there's a lot we learn each week and to be honest, there is not enough time to practice each technology before we move to the next one. Which is fair, because [cloud](https://www.cloudflare.com/en-gb/learning/cloud/what-is-the-cloud/) is **huge**, and in one semester, there is just enough time to scratch the surface.

I started my last set of contributions by adding a **Continuous Integration** pipeline to a [Javascript Project](https://github.com/mcruzdev/platformoon-web), so I could practice my **newly acquired** skill. Following the same principle, I decided to [Dockerize](https://www.techslang.com/definition/what-is-dockerization/) a project this time, which would be a step up from adding a basic `CI pipeline`.

## Table of Contents
&nbsp;1. [The Project](#the-project)
&nbsp;2. [Adding Dockerfiles 🚀](#adding-dockerfiles)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2.1. [Frontend](#frontend)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a. [Stage 1](#stage-1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b. [Stage 2](#stage-2)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c. [Stage 3](#stage-3)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2.2. [Server](#server)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; a. [Stage 1](#stage-1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b. [Stage 2](#stage-2)
&nbsp;3. [Setting up docker-compose](#setting-up-dockercompose)
&nbsp;4. [Pull Request(s)](#pull-request)
&nbsp;5. [Conclusion 🎇](#conclusion)
&nbsp;6. [Attributions](#attributions)

## The Project

After searching for hours and hours, I finally found an [issue](https://github.com/danielmarv/ChatRoom/issues/3) requesting to **Dockerize** the application.
It was a **full-stack** social media application called [ChatRoom](), with a [React](https://react.dev/) Frontend and an [Express.js](https://expressjs.com/) Backend. Even though the project was fairly new, and not super complex, it was exactly what I was looking for this time.

![ChatRoom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2n26sxc2gj68mzd7l7lx.png)

This is what the issue asked for

![Issue descripion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9nqbi4g3jphs6f6r2olv.png)

Therefore, the first step was to add [Dockerfiles](https://docs.docker.com/engine/reference/builder/) for both Frontend and the Backend.

## Adding Dockerfiles 🚀 

I started out by setting up and running the projects manually to understand the environment and library requirements for both projects.

![Both Projects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9pukjjf9xdtf0zccy1ii.png)

This wasn't particularly hard as I was already pretty comfortable with both **React** and **Express**. As soon as I could run both of them locally, it was time to start writing the Dockerfiles that would be used to build [Docker Images](https://docs.docker.com/engine/reference/commandline/images/).

### Frontend

The very first step I take whenever adding a Dockerfile, is to add a [.dockerignore](https://docs.docker.com/engine/reference/builder/#dockerignore-file) file to avoid transferring unnecessary files to the build context.

[**.dockerignore**](https://github.com/danielmarv/ChatRoom/blob/main/client/.dockerignore)
```
node_modules/
build/
.gitignore
```

Let's jump at the actual **Dockerfile** now.

I made sure to follow most of the **best practices** I recently learnt, and wrote a [multi-stage build](https://docs.docker.com/build/guide/multi-stage/) to end up with the smallest possible final image.

#### Stage 1

In the first stage, I used a heavier [base image](https://docs.docker.com/build/building/base-images/) to make sure that any utilities needed while installing the dependencies were available.

**Note:** I am starting my **stage count** from 0 in the `Dockerfile` comments.

```Dockerfile
###################################################################################################################
### STAGE 0: Install Dependencies ###
FROM node:20.9.0-bullseye@sha256:33e271b0c446d289f329aedafc7f57c41b3be1429956a7d1e174cfce10993eda AS dependencies
WORKDIR /app

# Copy package files
COPY package.json package-lock.json ./

# Clean install node dependencies defined in package-lock.json excluding dev dependencies
RUN npm ci

###################################################################################################################
```

#### Stage 2

In the next stage, I copied all the dependencies installed in `node_modules` from previous stage, transferred the source code from build context, and built the application in the last [layer](https://docs.docker.com/build/guide/layers/). This would bundle the application in an `/app/build/` directory, ready to be **served** to the users.

```Dockerfile
### STAGE 1: Build ###
FROM node:20.9.0-bullseye@sha256:33e271b0c446d289f329aedafc7f57c41b3be1429956a7d1e174cfce10993eda AS build

WORKDIR /app

# Copy the generated dependenices
COPY --from=dependencies /app /app

# Copy source code
COPY . .

# Build the application
RUN ["npm", "run", "build"]

###################################################################################################################
```

#### Stage 3

In the final stage, I deployed the application using [nginx](https://hub.docker.com/_/nginx), a **production grade** web server with various optimizations applied, including **efficient caching**.

```Dockerfile
###################################################################################################################

### STAGE 2: Deploy ###
FROM nginx:1.24.0-alpine@sha256:62cabd934cbeae6195e986831e4f745ee1646c1738dbd609b1368d38c10c5519 as deploy

# Copy bundled artifacts into nginx serve directory
COPY --from=build /app/build/ /usr/share/nginx/html

# Nginx uses the port 80
EXPOSE 80

# Add a healthcheck layer
HEALTHCHECK --interval=10s --timeout=10s --start-period=10s --retries=3 \
    CMD curl --fail localhost:80 || exit 1

###################################################################################################################
```

And with that, I was able to build the image
```bash
docker build -t test-chatroom-ui .
```

and **spin up** a container with
```bash
docker run --rm -dit -p 3000:80 --name test-container-ui test-chatroom-ui
```

![Final UI Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mna96ulsoivunr2o1mpx.png)



### Server

Now let's take a look at the corresponding files for the **Backend**. You might have guessed, the first thing to do was adding a `.dockerignore` file, which was identical to the one for UI.

**.dockerignore**
```
node_modules/
build/
.gitignore
```

However, the `Dockerfile` would be different since the environment and server requirements vary.
This time, I only had **2 stages** as I used the **node** server instead of **nginx**.

#### Stage 1

The first stage, like last time, was to transfer the `package.json` and `package-lock.json` files from the **build context** and install required **dependecies**.

```Dockerfile
# Stage 0: Install Dependencies
# Pick a fatter 'node' base image
# This will have everything necessary to run node, pre-installed when we run the container
FROM node:20.9.0-bullseye@sha256:88893710dcd8be69730988c171d4a88ee71f14338b59a9a23a89ff22cecd6559 AS dependencies

# Use /app as our working directory
WORKDIR /app

# Copy the package.json and package-lock.json 
# from the build context into the image
COPY package.json package-lock.json ./

# Clean install node dependencies defined in package-lock.json excluding dev dependencies
RUN npm ci --only=production

###################################################################################################################
```

#### Stage 2

The second and the final stage focusd on running the application and serving it on port expected by the **project maintainer**.

```Dockerfile
# Stage 1: Run and serve the application

# Use a thinner version of os for the final image
FROM node:20.9.0-alpine@sha256:d18f4d9889b217d3fab280cc52fbe1d4caa0e1d2134c6bab901a8b7393dd5f53 AS run

# Use /app as our working directory
WORKDIR /app

# Copy the generated dependenices
COPY --from=dependencies /app /app

# Copy src to /app/src/
COPY . .

# Environment variables
ENV MONGO_URL='secret'
ENV JWT_SECRET='secret'
ENV PORT=3001

# Build Args
ARG PORT=3001

# Start the container by running our server
# This is the default command that is run whenever a container is run using an image. 
# It can be overridden by providing the custom command as the second positional argument. 
CMD ["npm", "start"]


EXPOSE ${PORT}

# Add a healthcheck layer
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3\
	CMD curl --fail localhost:${PORT} | exit 1 

###################################################################################################################
```

You'll notice I have the secrets exposed in a file that will go to **Github**, which is **bad**, but we can fix that in later iterations (The maintainer had already exposed them).
You'll also notice a [Healthcheck Layer](https://docs.docker.com/engine/reference/builder/#healthcheck) which I forgot to discuss about for the UI. What it is doing is **fire** a request to localhost:3001 every **30s**, and report the status of container (**healthy, unhealthy, or waiting**) based on the response with a maximum of **3 retries**.

To spin up a container, the steps were identical to last time and everything worked perfect.

## Setting up docker-compose

Now that I was able to build images and run containers manually, it was time automate the process of **spinning up** and **tearing down** the containers. You might have noticed that to run a docker container, we need to type **super long commands** each time, which can get pretty annoying over time.

From the official [documentation](https://docs.docker.com/compose/),
> Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application's services. Then, with a single command, you create and start all the services from your configuration.

And for our application, we need to configure 2 services. One would be the **React Frontend** and another would be **Express Backend**.

Here's the configuration I came up with

[**docker-compose-yml**](https://github.com/danielmarv/ChatRoom/blob/main/docker-compose.yml)
```yaml
services:
  server:
    container_name: server
    build: ./server
    ports:
      - '3001:3001'

  client:
    container_name: client
    build: ./client
    ports:
      - '3000:80'
```

And with that, I was able to **spin up** containers simply using
```
docker-compose up
```

![Docker Containers Running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4sholwqr5x7w58nozb7s.png)

Notice the logs from both **client** and **server**. We can also spin them in **detached mode** like we did in the manual command using

```
docker-compose up -d
```

And to **tear** them down

```
docker-compose down
```

![Tearing down containers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uicg7qxsgpgvcy2aruw0.png)

## Pull Request

I opened the following **pull request** after testing locally, and optimizing image builds.
[https://github.com/danielmarv/ChatRoom/pull/5](https://github.com/danielmarv/ChatRoom/pull/5).
Which was merged by next morning.

![Pull Request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/imwmzno93n7r27poiclf.png)

Apart from this, I had also opened another pull request to a project which wasn't active for a while.
[https://github.com/leodube/PenaltyBias/pull/44#issuecomment-1821842350](https://github.com/leodube/PenaltyBias/pull/44#issuecomment-1821842350)

I don't expect this one to get merged anytime soon. I forgot to check the **project activity** before contributing.
![Second Pull Request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ua85wsla66z6106y8mis.png)

## Conclusion 🎇

In this post, I shared my experience Dockerizing a couple of open source projects. Not only I was able to add value to the community, but also got to practice writing **Dockerfiles** and working with **docker-compose**, which I learnt recently.

All in all, it was a pretty **worthwhile** experience and I'll be following up with another similar post soon.

In the meantime, stay tuned!

## Attributions
Cover Image by <a href="https://pixabay.com/users/planet_fox-4691618/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=6271649">Alexander Fox | PlaNet Fox</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=6271649">Pixabay</a>