Deploying our production-ready React app to S3

In part 1, we have set up a simple react app with all the components that you typically find in a high quality, production-ready react app such as unit test, git pre-commit hooks, flow type, etc… All that is left is to deploy our app and set up a robust continuous integration / deploy system. One of the main design goal for vudo app is to only pay for hosting when the app is being used, therefore we’ll use S3 to host our app and cloudfront CDN to make it fast.


  1. Sign up for AWS account. Get IAM access key & secret key. For detail instructions, see https://docs.aws.amazon.com/rekognition/latest/dg/setting-up.html
  2. Download aws-cli and configure authentication, https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html

Set up static website hosting with S3 & Cloudfront

Here is the official AWS tutorial how to do this

https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html and to speed up with cloudfront https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html

While we’re at it, let’s add an SSL certificate so we can access our app via https. This is a production-ready React app after all. Best of all, AWS gives our free SSL cert to be used with its services. Follow this article to add SSL cert to your cloudfront distribution https://aws.amazon.com/blogs/aws/new-aws-certificate-manager-deploy-ssltls-based-apps-on-aws/

To verify that your set up work, upload a blank index.html to the S3 bucket then go to your domain, e.g https://vudoapp.com. If the browser shows a blank page, we can proceed to next step

Deploying to S3

Our deploy strategy

To understand what we’re doing next, let’s go over the files that make up a typical react app and what we need to do to deploy to our S3 bucket. A typical single page application contains:

  1. index.html
    This is the page that the browser will render. It will have a script tag at the end to load our javascript bundle
  2. Javascript bundle
    This is usually a minified javascript file that contains our application code and all the necessary libraries bundled together. Webpack will generate this file for us
  3. Assets like CSS, etc…

In theory, the deploy could be as simple as uploading all the above files to S3. However, because we use Cloudfront as edge caching, it will not fetch the new files from S3 but instead serve up an old version that it already cached. To make sure our end users always see new version if there is one yet still fetch cached version from cloudfront if there hasn’t been any changes, we can do either:

  1. Add cache busting value into file name. If we can add a unique string to the name of our file that will only change when the content of the file changes, Cloudfront will treat the file as a new file because of its new name, and will fetch it from S3. Usually the content of the file is hashed with md5 and the resulted digest is added to the file name
  2. Invalidate old cache from cloudfront. Cloudfront lets you manually or programmatically remove a file from its cache. Next time the same file is requested, Cloudfront will fetch it from S3

For our app, we will do both. The reason is webpack makes it really easy to add cache busting value to file name. It can also build for us a new index.html from a template file with the script tag pointing to a new version of our bundle js. Because the index.html is changed, we’ll invalidate Cloudfront cache so the new file is fetched from S3

Enough theory, let’s get this baby deployed, shall we?

It’s coding time

We will need a few new dependencies

We need aws-sdk and s3 to upload file to S3 and invalidate Cloudfront cache. html-webpack-plugin creates new html file that will serves our new js bundle. cross-env & rimraf are two utility library for our build process

First, we need to let webpack know when it needs to build a production version of our file. Add  export const IS_PROD = process.env.NODE_ENV === 'production'; to config.js

Change webpack.config.babel.js to

Because we will use html-webpack-plugin to dynamically insert script tag, we can remove it from our index.html. Before moving on, let’s verify that development server is still working. Run yarn start and make sure http://localhost:5000 still works

Deploy script

To deploy, we will need to upload webpack-generated files to S3 and invalidate index.html from Cloudfront. I couldn’t find any package that achieve the same thing, so I wrote my own version. Let’s add scripts/deploy_s3.js

Add the following script to package.json

Note: Make sure to replace <S3 bucket name> and <Cloudfront distribution id> with actual value from above. While hard coding s3 bucket name and cloudfront distribution will work for now, it can present a problem with we create/destroy environments. Another approach would be to provision the environment using tools like Cloud Formation or Terraform and query the value at runtime

To deploy, run yarn test:deploy. If things go well, you should see something like this

You should be able to access your site by going to the host that was set up in step 1

Continuous integration and deploy with CircleCI

Continuous integration and deploy (CI/CI) is a practice of merging your change to main develop branch and deploying it to test environment as soon as it’s ready. The benefits of CI/CD includes

  1. Bugs are detected early and easy to track down due to small change set
  2. When bug occurs, we can revert to a known working version. This only affect a small number of changes
  3. Encourage developers to check in small and modular changes

However, to do CI/CD right, there are a few best practices

  1. Build should be fully automated. The moment we push a change to develop branch, our CI/CD system of choice should start building
  2. Good test coverage. Because CI/CD build and deploy each change immediately, unit test and acceptance test needs to be up to par to detect any errors before or shortly after the deploy

We will use CirclrCI as our CI/CD of choice. It has a free tier even for closed source project. To sign up, go to https://circleci.com and click either Sign Up with Github or Bitbucket depending on where the code is hosted

Set up new project on CircleCI

CircleCI sign up screen

To set up a new project, click Projects on the left side bar then Add Project button

CircleCI projects page

Select the project and click Setup project

CircleCI add project page

In the next screen, select correct language and follow the instruction. Create a .circleci folder and add a config.yml file with the following content

Commit the change and push to develop. Then click Start building

circleci setup project page

If things go well, the deploy step will fail and you will see something like this

The reason is CircleCI doesn’t have permission to upload files to our S3. To fix, create an IAM user with the following permissions

  1. s3:PutObject: Restrict this to the bucket that we set up in step 1
  2. cloudfront:CreateInvalidation: You can’t restrict this to just one cloudfront distribution so no restriction is here fine

Then go to project settings in CircleCI by clicking on the Gear icon in the top right hand corner. Go to AWS Permissions and copy and paste the Access Key & Secret Access Key there.

To rebuild, click Workflow on the left side bar and voila! A green build

Circleci successful build


You now have a production-ready react app hosted on S3 with Cloudfront CDN and a full CI/CD pipeline. Best of all, you only have to pay for the resource you use so the cost for this set up is next to nothing. If there are any steps that you think is essential in making a front end application production-ready, please comment below!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.