Complete guide on deploying a Docker application (React) to AWS Elastic Beanstalk using Docker Hub and Github Actions
Tags: react, docker, beanstalk, github
Published: 2021-08-31 10:11:21

I recently went through the struggle of setting up a pipeline for deploying a dockerized react app to Beanstalk and the whole process has left me with a new found respect for the magicians we sometimes take for granted, dev-ops engineers. 

In this article I will go over the process I used to deploy a Docker image to beanstalk using Docker Hub for hosting the image and GitHub Actions for building and orchestrating the whole process. In my journey I discovered that there are multiple ways of achieving this and there really isn't a "best" approach for all use cases. However, my familiarity with beanstalk and fear of getting started with a new technology like ECS was the main motivation behind using this approach. So if you're anything like me keep reading.

I have broken the whole process into smaller, independent (somewhat) steps that give a clear, high-level picture of the entire pipeline from setting up Docker to having the image running on Beanstalk.

# Pipeline Steps

1. Create necessary accounts
2. Dockerizing your application
3. Building the image on Github using Actions and pushing to Docker Hub
4. Deploying Docker Hub image to AWS Elastic Beanstalk
5. Making Docker repository private (Optional)

![Paper.Journal.6.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcq7nefs6u2s8ka2xc6m.png)

# Create necessary accounts

Let's sign up for all the services that we'll need for this setup.
1. Github 
2. Docker Hub
3. Amazon Web Services (AWS)

# Dockerizing your application

## Why Docker?

Why use Docker? Good question. In my opinion it's the closest you can be to sharing a single "machine" for development with your all your peers. If this answer doesn't appeal to you then I sincerely urge you to read more on this topic as there are plenty of articles written by more qualified developers talking about why you should use Docker and how it can make your life easier.

## Setting Up Docker

Now that you're convinced, let's go over the docker configurations. For the purpose of this article, I'm going to assume that you already have a basic react (or any other) application set up with docker that you can start by building the image and running the container. If you don't have it set up then you can start with create-react-app and then add docker manually or clone a boilerplate like [this one](https://github.com/akash4393/react-typescript-redux-docker).

Here's what the Dockerfile for my react application looks like:
```dockerfile
# Dockerfile

# pull official base image
FROM node:13.12.0-alpine

# set working directory
WORKDIR /app

# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH

# install app dependencies
COPY package.json ./
COPY package-lock.json ./
RUN npm install

# start app
CMD ["npm", "run", "start"]

# expose port
EXPOSE 3000
```

Now that you have a dockerized application, let's create docker configurations for production server which uses nginx reverse-proxy web server (I named it Dockerfile.prod). 

```dockerfile
# Dockerfile.prod

# build environment
FROM node:13.12.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm ci
COPY . ./
RUN npm run build

# production environment
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
# to make react-router work with nginx
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] 
``` 
<sub>Note: If you're not sure why we use nginx, then I highly recommend reading more about it.</sub>

This basically creates an image for my application and runs the build application script. It then copies the files generated by the build script (<code>npm run build</code>) to the nginx image under the <code>/usr/share/nginx/html</code> location, exposes port 80 and starts the server.

Here's an optional docker-compose file that I use along with the Dockerfile to build & test build script code in local. I use it by running <code>docker compose -f docker-compose.prod.yml up --build</code> and then going to localhost:80 in my browser. <br /><sub>You don't need this in order to continue with this tutorial.</sub>

```YAML
# docker-compose.prod.yml

version: '3.8'

services:
  frontend-prod:
    container_name: frontend-prod
    build:
      context: .
      dockerfile: Dockerfile.prod
    ports:
      - '80:80'
```

# Building the Image on Github Using Actions and Pushing to Docker Hub

Now let's set up Github Actions to build the production docker image whenever you push code to a branch and then deploy that image to Docker Hub. I'm assuming that you have already set up a github account and are able to push code to your repo. If you haven't done that yet, then you should stop here, create a github account, push your code to a new repo and then come back.

## Github Actions YAML File

Github Actions works by reading a <code>.github/workflows/<your workflow name>.yml</code> file in your codebase for instructions. It uses the basic YAML syntax which is human friendly and should be fairly easy to get follow. Let's create this YAML file from Github's UI and add some basic steps.


![create-action-github.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/up8cl3x97c6tlyi3lqa8.gif)


We're going to add the following content to the file:
```YAML
name: Dev deployment from Github to AWS
on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Latest Repo
        uses: actions/checkout@master
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1
      
      - name: Login to DockerHub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}
      
      - name: Build and push
        uses: docker/build-push-action@v2
        with:
          context: .
          file: Dockerfile.prod
          push: true
          tags: <your repo name>/<your application name>:latest

```

Here we're setting the name of the action which you can see on line 1. Then we say that on <code>push</code> event on <code>main</code> branch we're going to run the jobs defined below. Next we define the <code>deploy</code> job which runs on <code>ubuntu-latest</code> machine. Then we define the follwing steps:
1. Use <code>actions/checkout@master</code> action to checkout the <code>main</code> branch
2. Use <code>docker/setup-buildx-action@v1</code> action to set up Buildx, the tool we'll use to push the image to Docker Hub
3. Use <code>docker/login-action@v1</code> action to authenticate with Docker Hub. We'll set secrets object will in the repository settings on Github in the next step.
4. Use <code>docker/build-push-action@v2</code> action to build the image using <code>Dockerfile.prod</code> file and then push it as <code><your repo name>/<your application name></code> on Docker Hub with the <code>latest</code> tag.

Now that we have added these steps to the <code>main.yml</code> file, let's commit the changes and go back to our local machine and pull the latest.

## Github Secrets

Now let's create the secrets in our Github repo.

![github-secrets.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqarivquzu1ijcw2ekwk.gif)

Use the same process to add the <code>DOCKERHUB_PASSWORD</code> secret.

## Running the Action

Now that we have everything set up, let's make some minor code changes in the <code>main</code> branch and push. Once you do, you should be able to navigate to the actions tab on Github and see the <code>deploy</code> action running. It should look something like this.

![deploy-action-github.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qopvi8zep8usc50jzmcu.gif)

Once the action is complete, open your Docker Hub account and make sure that the image was pushed succesfully.

# Deploying Docker Hub image to AWS Elastic Beanstalk

In this section we're going to set up AWS to deploy the Docker Hub image to Elastic Beanstalk and have our application available on the world wide web! We'll achieve this by sending instructions to beanstalk from Github Actions to pull and run the image from Docker Hub.

## Setting Up AWS

Before getting started, you should complete creating an account on AWS and then setting up payments, etc. to be able to create a Beanstalk application. 

### Creating Beanstalk Admin User for Deployments

Follow these steps to create an IAM user with programatic access that we'll use to deploy packages to our Beanstalk application from Github Actions:
1. Navigate to IAM
2. Create a new Group (We'll call it <code>beanstalk-admin</code> here)
3. Add the <code>AdministratorAccess-AWSElasticBeanstalk</code> permission to the <code>beanstalk-admin</code> group

It should look something like this:

![user-group-permission.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf8ct2imqv14bh04b4y1.png)

<sub>Ignore the other permission for now.</sub>

4. Create a new user called <code>github-deployment-user</code>
5. Give Programatic Access to this user and add it to the <code>beanstalk-admin</code> group
6. Copy the <code>Access key ID</code> and the <code>Secret access key</code>. We're going to need these later

![create-user-aws.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55dj6hyln7b35wbqwlls.gif)

### Create Beanstalk Application

Let's create a new Beanstalk application that we'll deploy to. Navigate to Elastic Beanstalk, click Create Application, name the application, and then set the platform as Docker and leave everything else as default. 

![create-eb-application.gif](https://cdn.hashnode.com/res/hashnode/image/upload/v1628779847585/Rps2fW95O.gif)

Now that we have everything set up on AWS, let's create the instructions file that will tell beanstalk to pull & run the right image from Docker Hub. 

### Create Dockerrun.aws.json

Beanstalk can work with docker-compose or Dockerrun.aws.json for getting instructions on what image to deploy but to keep things simple and set us up for the last step of this pipeline we're going to be using the Dockerrun.aws.json file. You should create this file at the root of your project folder.

```JSON
// Dockerrun.aws.json

{
    "AWSEBDockerrunVersion": "1",
    "Image": {
        "Name": "<your repo name>/<your application name>:latest",
        "Update": "true"
    },
    "Ports": [
        {
            "ContainerPort": "80"
        }
    ],
    "Logging": "/var/log/nginx"
}      
```

Here we're telling beanstalk to pull <code><your repo name>/<your application name>:latest</code> image and then expose <code>PORT 80</code> of the container. 

### Update Github Actions to Send Instructions to Beanstalk

Now we're going to update the Github Actions YAML file we added earlier. Let's add the following steps to our existing <code>deploy</code> job:
```YAML
# .github/workflows/main.yml continued

      - name: Get Timestamp
        uses: gerred/actions/current-time@master
        id: current-time
      
      - name: Run String Replace
        uses: frabert/replace-string-action@master
        id: format-time
        with:
          pattern: '[:\.]+'
          string: "${{ steps.current-time.outputs.time }}"
          replace-with: '-'
          flags: 'g'
      
      - name: Generate Deployment Package
        run: zip -r deploy.zip * -x "**node_modules**"

      - name: Deploy to EB
        uses: einaregilsson/beanstalk-deploy@v16
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: <beanstalk application name>
          environment_name: <beanstalk environment name>
          version_label: "docker-app-${{ steps.format-time.outputs.replaced }}"
          region: us-west-2
          deployment_package: deploy.zip
```

Here we're adding the following steps:
1. Get the current timestamp (this is for tracking the version on beanstalk)
2. Use action <code>frabert/replace-string-action@master</code> to replace <code>:</code> with <code>-</code> in the timestamp string (optional)
3. Create a zipped package <code>deploy.zip</code> of our codebase excluding <code>node_modules</code> folder. Note: We're doing this to send the Dockerrun.aws.json which is at the root of our project to beanstalk.
4. Use action <code>einaregilsson/beanstalk-deploy@v16</code> to push the zip to beanstalk. Make sure that you set the right values for <code>application_name</code>, <code>environment_name</code>, and <code>region</code>

As you might have guessed looking at the steps, we'll need to add <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> secret keys to our Github repository. <code>AWS_ACCESS_KEY_ID</code> is the <code>Access Key ID</code> and <code>AWS_SECRET_ACCESS_KEY</code> is the <code>Secret access key</code> for the <code>github-deployment-user</code> that we created on step 6 in the Creating Beanstalk Admin User for Deployments secton.

Now that you have added the secrets to the Github repo, go ahead and commit & push the updated <code>main.yml</code> and the newly added <code>Dockerrun.aws.json</code> files. This should start a new <code>deploy</code> job under the Actions tab with the commit message as the title. Expand the job to make sure that you see the new steps that you added to your <code>main.yml</code> file.

Once the job completes, and if everything goes well, you should have your application deployed successfully in beanstalk and you should be able to access it by going to the beanstalk instance's public URL.<br /><sub>Note: If some thing breaks on AWS and you see the health of you application is red, then go to the <code>Logs</code> tab and download full logs. Unzip the package and look at <code>eb-engine.log</code> file. You should be able to find what went wrong by searching for the <code>[ERROR]</code> line in there.</sub>

![download-logs-beanstalk.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628779971706/mrqNV1eoJ.png)

# Making Docker repository private (Optional)

Now let's talk about how we can make our Docker Hub repository private. So far our application was publicly available on Docker, meaning that anybody will be able to find and pull this image from Docker Hub. In this section we'll go over making the Docker repo private and authorizing our beanstalk instance to be able to pull the image from Docker Hub.<br /><sub>I struggled a bit with this part due to outdated documentation on Amazon's site and not having any recent answers from the community on Stackoverflow.</sub>

Here's basically what we need to do:
1. Make the Docker repository private by going to Docker Hub and changing the settings. 
2. Create an authorization token and save it in a file that beanstalk can read.
3. Save that file on S3 so that our <code>github-deployment-user</code> can access it during deployment.
4. Update the <code>Dockerrun.aws.json</code> file to use the autorization token we created on step 2.

## Making Docker Repo Private

This is pretty straight forward. You go to Docker Hub, then find the repository, go to settings and make it private. Docker gives you 1 free private repository with each Docker Hub account.

## Getting the Authorization Token and Saving in a File

We'll call this file <code>dockercfg</code> because that is what the documentaiton keeps calling it but feel free to name it anything you want, like <code>my-nightmare-file</code>. This is what the contents of the file should look like:
```JSON
{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "<your auth token>"
    }
  }
}
```

Now the easiest way to create this file is by running the command <code>docker login</code> in your terminal and then copying the <code>auths</code> object from <code>config.json</code> file stored in <code>~/.docker/</code> folder (Windows folk, please Google the location). Now the problem here is that on macOS you'll see something like this:
```JSON
{
  "auths": {
    "https://index.docker.io/v1/": {}
  },
  "credsStore": "desktop",
  "experimental": "disabled",
  "stackOrchestrator": "swarm"
}
```

This is because docker is using your keychain api to securely store the auth token instead of writing it to a file. Which is great, until you need the token. But thanks to the power of [Stackoverflow](https://stackoverflow.com/a/45671103), I learned that you can generate the authorization string by running this in your terminal: 
```bash
echo -n '<docker hub username>:<docker hub password>' | base64
```

Once you have this, create the <code>dockercfg</code> file as above (with the auth token) and save it on your computer. We're going to update some AWS configurations and upload it to S3 next.

## Uploading dockercfg to S3

Now you need to upload this file to S3 bucket that is in the same region as our beanstalk application and add S3 read access to our <code>github-deployment-user</code> IAM user so that the beanstalk application can read the file.

### AWS Setup

To add permission to our user, go to IAM then go to user groups, select the <code>beanstalk-admin</code> group and add the  <code>AmazonS3ReadOnlyAccess</code> permission. 

![user-group-permission-read-s3.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1628779871966/1EIQXoxRH.png)

### Uploading to AWS

Now let's go to S3 and create a new bucket. We'll call it <code>docker-login-bucket</code> but you can call it anything you like. Make sure you uncheck the <code>Block all public traffic</code> option. Once the bucket is created we'll upload the <code>dockercfg</code> file which we created in the previous step. On the Upload page, after you select the file expand the Permissions section and select <code>Specify Individual ACL Permissions</code> and after that enable both Read access for the third option, <code>Authenticated User Groups</code>. This will allow our beanstalk user to read the contents of this file.

![save-in-s3.gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t7s7cvbkkisj7ehoro13.gif)

## Updating Dockerrun.aws.json

Now we need to tell beanstalk that our Docker repository is private and point it to the <code>dockercfg</code> file so that it can use the authorization token when pulling the image from Docker Hub.

We'll add an authentication object to the <code>Dockerrun.aws.json</code> file which will point to the <code>dockercfg</code> file in S3.
```JSON
"Authentication": {
  "bucket": "docker-login-bucket",
  "key": "dockercfg"
},
```

After adding this, the <code>Dockerrun.aws.json</code> file should look like this
```JSON
{
  "AWSEBDockerrunVersion": "1",
  "Authentication": {
    "bucket": "docker-collaborationteamsad-auth",
    "key": "dockercfg"
  },
  "Image": {
    "Name": "collaborationteamsad/sparks-dev-fe:latest",
    "Update": "true"
  },
  "Ports": [
    {
      "ContainerPort": "80"
    }
  ],
  "Logging": "/var/log/nginx"
}      
```

Now that we've updated the <code>Dockerrun.aws.json</code> file, let's push the code Github and check the new <code>deploy</code> action that gets created. If everything was set up correctly then the pipeline should complete successfully and you should be able to see your application running on the beanstalk application URL. 

Congratulations! You have successfully set up a pipeline to build a Docker image using Github Actions, store it in a private repository on Docker Hub and then deployed it on an AWS Elastic Beanstalk application. Every push to the <code>main</code> branch should now successfully deploy your code. 🎉 

### Update - 8/28/2021

If you want to use <code>docker-compose.yml</code> instead of <code>Dockerrun.aws.json</code> for deploying then follow the steps provided [here.](https://akashsingh.blog/deploying-private-docker-image-to-aws-beanstalk-amazon-linux-2-using-docker-composeyml)