DEV Community

Cover image for Deploy a React App on AWS Lightsail: Testing, Docker, Docker Compose, Nginx & Github Actions
Mangabo Kolawole
Mangabo Kolawole

Posted on • Updated on

Deploy a React App on AWS Lightsail: Testing, Docker, Docker Compose, Nginx & Github Actions

So you have written your React Application and you are ready to deploy it?

Although there are already existing solutions like Netlify, Vercel, to help you deploy your application easily and quickly, it's always good for a developer to know how to deploy an application on a private server.

Today, we'll learn how to deploy a React App on AWS Lightsail. This can also be applied to other VPS providers.

Table of content

  • Setup
  • Prepare the React application for deployment
  • Environment variables
  • Testing
  • Docker Configuration
  • Github Actions (testing)
  • Preparing the server
  • Github Actions (Deployment)

1 - Setup

For this project, we'll be using an already configured React application. It's a project made for this article about FullStack React & React Authentication: React REST, TypeScript, Axios, Redux & React Router.

You can directly clone the repo here.

Once it's done, make sure to install the dependencies.

cd django-react-auth-app
yarn install
Enter fullscreen mode Exit fullscreen mode

2 - Prepare application for deployment

Here, we'll configure the application to use env variables but also configure Docker as well.

Env variables

It's important to keep sensitive bits of code like API keys, passwords, and secret keys away from prying eyes.
The best way to do it? Use environment variables. Here's how to do it in our application.

Create two files :

  • a .env file which will contain all environment variables
  • and a env.example file which will contain the same content as .env.

Actually, the .env file is ignored by git. The env.example file here represents a skeleton we can use to create our .env file in another machine.

It'll be visible, so make sure to not include sensitive information.

# ./.env
REACT_APP_API_URL=YOUR_BACKEND_HOST
Enter fullscreen mode Exit fullscreen mode

Now, let's copy the content and paste it in .env.example, but make sure to delete the values.

./env.example
REACT_APP_API_URL=
Enter fullscreen mode Exit fullscreen mode

Testing

Testing in an application is the first assurance of maintainability and reliability of our React server.
We'll be implementing testing to make sure everything is green before pushing for deployment.

To write tests here, we'll be using the react testing library.
We'll basically test the values in the inputs of your Login component.

// src/pages/Login.test.tsx
import React from "react";
import '@testing-library/jest-dom'
import {fireEvent, render, screen} from "@testing-library/react";
import Login from "./Login";
import store from '../store'
import {Provider} from "react-redux";

const renderLogin = () => {
    render(
        <Provider store={store}>
            <Login/>
        </Provider>
    )
}

test('Login Test', () => {
    renderLogin();
    expect(screen.getByTestId('Login')).toBeInTheDocument();

    const emailInput = screen.getByTestId('email-input');
    expect(emailInput).toBeInTheDocument();
    fireEvent.change(emailInput, {target: {value: 'username@gmail.com'}})
    expect(emailInput).toHaveValue('username@gmail.com');

    const passwordInput = screen.getByTestId('password-input');
    expect(passwordInput).toBeInTheDocument();
    fireEvent.change(passwordInput, {target: {value: '12345678'}})
    expect(passwordInput).toHaveValue('12345678');
})
Enter fullscreen mode Exit fullscreen mode

Now run the tests.

yarn test
Enter fullscreen mode Exit fullscreen mode

Now let's move to the Docker configuration.

Dockerizing our app

Docker is an open platform for developing, shipping, and running applications inside containers.
Why use Docker?
It helps you separate your applications from your infrastructure and helps in delivering code faster.

If it's your first time working with Docker, I highly recommend you go through a quick tutorial and read some documentation about it.

Here are some great resources that helped me:

Dockerfile

The Dockerfile represents a text document containing all the commands that could call on the command line to create an image.

Add a Dockerfile.dev to the project root. It'll represent the development environment.

# Dockerfile.dev

FROM node:14-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
Enter fullscreen mode Exit fullscreen mode

Here, we started with an Alpine-based Docker Image for JavaScript. It's a lightweight Linux distribution designed for security and resource efficiency.

Also, let's add a .dockerignore file.

node_modules
npm-debug.log
Dockerfile.dev
Dockerfile.prod
.dockerignore
yarn-error.log
Enter fullscreen mode Exit fullscreen mode

Docker Compose

Docker Compose is a great tool (<3). You can use it to define and run multi-container Docker applications.

What do we need? Well, just a YAML file containing all the configuration of our application's services.
Then, with the docker-compose command, we can create and start all those services.

Here, the docker-compose.dev.yml file will contain three services that make our app: nginx and web.

This file will be used for development.

As you guessed :

version: "3"

services:

  nginx:
    container_name: core_web
    restart: on-failure
    image: nginx:stable
    volumes:
      - ./nginx/nginx.dev.conf:/etc/nginx/conf.d/default.conf
    ports:
      - "80:80"
    depends_on:
      - web
  web:
    container_name: react_app
    restart: on-failure
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src
    ports:
      - "3000:3000"
    command: >
      sh -c "yarn start"
    env_file:
      - .env
Enter fullscreen mode Exit fullscreen mode
  • nginx: NGINX is open-source software for web serving, reverse proxying, caching, load balancing, media streaming, and more.
  • web: We'll run and serve the endpoint of the React application.

And the next step, let's create the NGINX configuration file to proxy requests to our backend application.
In the root directory, create a nginx directory and create a nginx.dev.conf file.

upstream webapp {
    server react_app:3000;
}
server {

    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://webapp;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

}
Enter fullscreen mode Exit fullscreen mode

Docker Build

The setup is completed. Let's build our containers and test if everything works locally.

docker-compose -f docker-compose.dev.yml up -d --build 
Enter fullscreen mode Exit fullscreen mode

Once it's done, hit localhost/ to see if your application is working.
You should get a similar page.

Login Page

Great! Our React application is successfully running inside a container.

Let's move to the Github Actions to run tests every time there is a push on the main branch.

Github Actions (Testing)

GitHub actions are one of the greatest features of Github. it helps you build, test or deploy your application and more.

Here, we'll create a YAML file named main.yml to run some React tests.

In the root project, create a directory named .github. Inside that directory, create another directory named workflows and create the main.yml file.

name: React Testing and Deploying

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    name: Tests
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2

      - name: Installing dependencies
        run: yarn install

      - name: Running Test
        run: yarn test
Enter fullscreen mode Exit fullscreen mode

Basically, what we are doing here is setting rules for the GitHub action workflow, installing dependencies, and running the tests.

  • Make sure that this workflow is triggered only when there is a push or pull_request on the main branch
  • Choose ubuntu-latest as the OS and precise the Python version on which this workflow will run.
  • After that as we install the javascript dependencies and just run the tests.

If you push the code in your repository, you'll see something similar when you go to your repository page.

React Actions

After a moment, the yellow colors will turn to green, meaning that the checks have successfully completed.

Setting up the AWS server

I'll be using a Lightsail server here. Note that these configurations can work with any VPS provider.

If you want to set up a Lightsail instance, refer to the AWS documentation.

Personally, I am my VPS is running on Ubuntu 20.04.3 LTS.

Also, you'll need Docker and docker-compose installed on the machine.

After that, if you want to link your server to a domain name, make sure to add it to your DNS configuration panel.

DNS Configuration

Once you are done, we can start working on the deployment process.

Docker build script

To automate things here, we'll write a bash script to pull changes from the repo and also build the docker image and run the containers.

We'll also be checking if there are any coming changes before pulling and re-building the containers again.

#!/usr/bin/env bash

TARGET='main'

cd ~/app || exit

ACTION='\033[1;90m'
NOCOLOR='\033[0m'

# Checking if we are on the main branch

echo -e ${ACTION}Checking Git repo
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" != ${TARGET} ]
then
  exit 0
fi

# Checking if the repository is up to date.

git fetch
HEADHASH=$(git rev-parse HEAD)
UPSTREAMHASH=$(git rev-parse ${TARGET}@{upstream})

if [ "$HEADHASH" == "$UPSTREAMHASH" ]
then
  echo -e "${FINISHED}"Current branch is up to date with origin/${TARGET}."${NOCOLOR}"
  exit 0
fi

# If that's not the case, we pull the latest changes and we build a new image

git pull origin main;

# Docker

docker-compose -f docker-compose.prod.yml up -d --build

exit 0;
Enter fullscreen mode Exit fullscreen mode

Good! Login on your server using SSH. We'll be creating some new directories: one for the repo and another one for our scripts.

mkdir app .scripts
cd .scripts
vim docker-deploy.sh
Enter fullscreen mode Exit fullscreen mode

And just paste the content of the precedent script and modify it if necessary.

cd ~/app
git clone <your_repository> .
Enter fullscreen mode Exit fullscreen mode

Don't forget to add the dot .. Using this, it will simply clone the content of the repository in the current directory.

Great! Now we need to write the docker-compose.prod.yml file which will be run on this server.

We'll be adding an SSL certificate, by the way, so we need to create another nginx.conf file.

Here's the docker-compose.prod.yml file.

version: "3.7"

services:

  nginx:
    container_name: core_web
    restart: on-failure
    image: jonasal/nginx-certbot:latest
    env_file:
      - .env.nginx
    volumes:
      - nginx_secrets:/etc/letsencrypt
      - ./nginx/user_conf.d:/etc/nginx/user_conf.d
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web

  web:
    container_name: react_app
    restart: on-failure
    build:
      context: .
      dockerfile: Dockerfile.prod
    volumes:
      - ./src:/app/src
    ports:
      - "5000:5000"
    command: >
      sh -c "yarn build && serve -s build"
    env_file:
      - .env

volumes:
  nginx_secrets:
Enter fullscreen mode Exit fullscreen mode

If you noticed, we've changed the nginx service. Now, we are using the docker-nginx-certbot image. It'll automatically create and renew SSL certificates using the Let's Encrypt free CA (Certificate authority) and its client certbot.

And our React server is running the build app. Using yarn build, it'll create a production optimized app which we'll serve.

And finally, we'll add the Dockerfile.prod file

FROM node:14-alpine AS builder
WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
Enter fullscreen mode Exit fullscreen mode

Create a new directory user_conf.d inside the nginx directory and create a new file nginx.conf.

upstream webapp {
    server react_app:5000;
}

server {

    listen 443 default_server reuseport;
    listen [::]:443 ssl default_server reuseport;
    server_name dockerawsreact.koladev.xyz;
    server_tokens off;
    client_max_body_size 20M;


    ssl_certificate /etc/letsencrypt/live/dockerawsreact.koladev.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dockerawsreact.koladev.xyz/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/dockerawsreact.koladev.xyz/chain.pem;
    ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;

    location / {
        proxy_pass http://webapp;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
Enter fullscreen mode Exit fullscreen mode

Make sure to replace dockerawsreact.koladev.xyz with your own domain name...

And no troubles! I'll explain what I've done.

server {
    listen 443 default_server reuseport;
    listen [::]:443 ssl default_server reuseport;
    server_name dockerawsreact.koladev.xyz;
    server_tokens off;
    client_max_body_size 20M;
Enter fullscreen mode Exit fullscreen mode

So as usual, we are listening on port 443 for HTTPS.
We've added a server_name which is the domain name. We set the server_tokens to off to not show the server version on error pages.
And the last thing, we set the request size to a max of 20MB. It means that requests larger than 20MB will result in errors with HTTP 413 (Request Entity Too Large).

Now, let's write the job for deployment in the Github Action.

...
  deploy:
    name: Deploying
    needs: [test]
    runs-on: ubuntu-20.04
    steps:
      - name: SSH & Deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SSH_AWS_SERVER_IP }}
          username: ${{ secrets.SSH_SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          passphrase: ${{ secrets.SSH_PASSPHRASE }}
          script: |
            cd ~/.scripts
            ./docker-deploy.sh
Enter fullscreen mode Exit fullscreen mode

Notice the usage of Github Secrets here. It allows the storage of sensitive information in your repository. Check this documentation for more information.

We also using here a GitHub action that requires the name of the host, the username, the key, and the passphrase. You can also use this action with a password but it'll require some configurations.
Feel free to check the documentation of this action for more detail.

Also, notice the needs: [build] line. It helps us make sure that the precedent job is successful before deploying the new version of the app.

Once it's done, log via ssh in your server and create a .env file.

cd app/
vim .env # or nano or whatever
Enter fullscreen mode Exit fullscreen mode

And finally, create a .env.nginx file. This will contain the required configurations to create an SSL certificate.

# Required
CERTBOT_EMAIL=

# Optional (Defaults)
STAGING=1
DHPARAM_SIZE=2048
RSA_KEY_SIZE=2048
ELLIPTIC_CURVE=secp256r1
USE_ECDSA=0
RENEWAL_INTERVAL=8d
Enter fullscreen mode Exit fullscreen mode

Add your email address. Notice here that STAGING is set to 1. We will test the configuration first with Let’s encrypt staging environment! It is important to not set staging=0 before you are 100% sure that your configuration is correct.

This is because there is a limited number of retries to issue the certificate and you don’t want to wait till they are reset (once a week).

Declare the environment variables your project will need.

And we're nearly done. :)

Make a push to the repository and just wait for the actions to pass successfully.

Deployment

And voilà. We're done with the configuration.

HTTP Expired.

if your browser shows an error like this, the configuration is clean! We can issue a production-ready certificate now.
On your server, stop the containers.

docker-compose down -v
Enter fullscreen mode Exit fullscreen mode

edit your .env.nginx file and set STAGING=0.

Then, start the containers again.

sudo docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

And we're done. :)

Conclusion

In this article, we've learned how to use Github Actions to deploy a dockerized React application on an AWS Lightsail server. Note that you can use these steps on any VPS.

And as every article can be made better so your suggestion or questions are welcome in the comment section. 😉

Check the code of this tutorial here.

Discussion (12)

Collapse
tngeene profile image
Ted Ngeene

Great post! I can relate to it especially since your workflow is almost similar to mine.
I deploy Django apps in an almost similar flow, never deployed react like this though but it seems like an interesting challenge. Will definitely try lightsail 🚀⛵

Collapse
amrelmohamady profile image
Amr Elmohamady

Why would you use all that for a react app?! Like docker and nginx... Really!

Collapse
koladev profile image
Mangabo Kolawole Author

Hey Amr.

We can use VPS to deploy some frontend projects too. At my job, we even use linode, docker and caddy to deploy the management dashboard. :)

Collapse
tolsee profile image
Tulsi Sapkota

The fact that we could doesn't mean we should 🙂. Great post by the way, I am sure many people will learn alot of things from this.

But, I would like to point out that serving static react apps through cloudfront(Any other CDN) is way better for most of the cases.

Collapse
jlancelot2007 profile image
Andrew Plater

The point is to learn how to use Lightsail not React.

Collapse
amrelmohamady profile image
Amr Elmohamady

Then the tutorial should be with node not react!

Collapse
sm0ke profile image
Sm0ke

🚀🚀

Collapse
koladev profile image
Mangabo Kolawole Author

🚀

Collapse
zachjonesnoel profile image
Jones Zachariah Noel

Awesome! Never knew Lightsail could use containers also.

Collapse
koladev profile image
Mangabo Kolawole Author

Lightsail is pretty powerful. :)

Collapse
tsunade01 profile image
Jessica Ramos

Great Post!

Collapse
koladev profile image
Mangabo Kolawole Author

Thank you Jessica.:)