DEV Community

loading...

Deploy a React app to DigitalOcean using Github Actions and Docker

kenessajr profile image Remy Muhire ใƒปUpdated on ใƒป6 min read

At Pindo, we recently automated all our deployment processes by setting up continuous delivery pipelines for our repositories. This set up helped us reduce the number of errors that would otherwise occur because of the repetitive steps of Continuous Delivery (CD).
In this tutorial, you will learn how to set up a continuous delivery of a React app using tools like Docker and Github Actions. We will use an Ubuntu (18.04 LTS) droplet on DigitalOcean to host our app.

Prerequisites

Here are the prerequisites required for this tutorial.

Create your app

Use the officially supported create-react-app.dev to create a single-page React application. It offers a modern build setup with no configuration.

  • Install create-react-app
npm install -g create-react-app
  • Quick Start
npx create-react-app my-app && cd my-app
npm start

Dockerize your app.

Add a Dockerfile to the project root:

FROM node:13.1-alpine

WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn cache clean && yarn --update-checksums
COPY . ./
EXPOSE 3000
CMD ["yarn", "start"]

yarn cache clean running this command will clear the global cache.
yarn --update-checksums lock lockfile if there's a mismatch between them and their package's checksum.

Let's build and tag our docker image

docker build -t my-app:dev .

Run the container once the build is done

docker run -it -p 3000:3000 my-app:dev 

Boom ๐Ÿ’ฅ! Our app is running on http://localhost:3000

Let's create another Dockerfile-prod to the project root. You will use this file in production.

Dockerfile-prod:

FROM node:13.1-alpine as build

WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn cache clean && yarn --update-checksums
COPY . ./
RUN yarn && yarn build

# Stage - Production
FROM nginx:1.17-alpine
COPY --from=build /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

In this Dockerfile-prod we create a production build for our app and then copy the build file to nginx html directory.

Next, let's build and run our production image.

docker build -f Dockerfile-prod -t my-app:prod .
docker run -itd -p 80:80 --rm my-app:prod

Our app is now running on port 80. In the next segment, we will publish the image to Github Packages.

Publish Your Image to Github Packages.

Github Packages gives you the option to publish and consume packages within your business or worldwide. To realize this, we will create a Github Action which will publish our package to the Github Packages Registry. Before we deploy our production image to the registry, we need to make sure that our code is production-ready.

deploy.yml

Let's create our first deployment action in our project.

mkdir .github && cd .github && mkdir workflows && cd workflows && touch deploy.yml

The command above creates a workflow folder and a deploy.yml file. You can replace yarn with npm in the code below.

name: build

on:
  push:
    branches: 
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js 13.10
      uses: actions/setup-node@v1
      with:
        node-version: '13.10'
    - name: Install yarn and run unittest
      run: |
        yarn
        yarn test
      env:
        CI: true
    - name: Publish to Github Packages Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: my_github_username/my_repository_name/my_image_name
        registry: docker.pkg.github.com
        username: ${{ secrets.GITHUB_USERNAME }}
        password: ${{ secrets.GITHUB_TOKEN }}
        dockerfile: Dockerfile-prod
        tags: latest

Note that Github Actions automatically provides you with GITHUB_TOKEN secrets.

Repository

Add repository secrets

What are Secrets? They are encrypted environment variables that you create in a repository for use with GitHub Actions.

Next, let's add our GITHUB_USERNAME to the secrets.

Remember that GITHUB provides you with GITHUB_TOKEN by default.

Alt Secrets

Push to master

Let's recap. We completed setting up our secrets, created our remote repository, and set remote origins to our local repository. We are now ready to go ahead and push our changes to our remote repository.

git add -A
git commit -m "Initial commit"
git push origin master

If you click on actions, you will notice the start of the deployment workflow. Wait and see your image being published on your Github Packages Registry.

Alt actions

You can find your published docker image in your repository on the package tab.

Alt pack-det

We successfully published our docker app image on the Github Package Registry. We are going to order a Docker Droplet on DigitalOcean and set up a flow to deploy and our app image on DigitalOcean.

Deploy.

For deployment, we are going to create a Docker Droplet on DigitalOcean. Please do not forget to sign up with my Referral Link and get $100 in credit for over 60 days.

Alt docker-droplet

For this example, we access our droplet with a username and a password, please choose a one-time password over an SSH key.

Alt one-t-p

After configuring and resetting your droplet password let's now add your droplet secrets to your repository.

  • HOST: Droplet IP_ADDRESS
  • PASSWORD: Droplet PASSWORD
  • PORT: Droplet SSH port (22)
  • USERNAME: Droplet USERNAME

Update deploy.yml file.

You have succeeded in setting up your droplet secrets to your repository. You will now add another code block to deploy your package and run it in our droplet using ssh-action. It's GitHub Actions for executing remote ssh commands.

name: build

on:
  push:
    branches: 
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js 13.10
      uses: actions/setup-node@v1
      with:
        node-version: '13.10'
    - name: Install yarn and run unittest
      run: |
        yarn
        yarn test
      env:
        CI: true
    - name: Publish to Github Packages Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: my_github_username/my_repository_name/my_image_name
        registry: docker.pkg.github.com
        username: ${{ secrets.GITHUB_USERNAME }}
        password: ${{ secrets.GITHUB_TOKEN }}
        dockerfile: Dockerfile-prod
        tags: latest
    - name: Deploy package to digitalocean
      uses: appleboy/ssh-action@master
      env:
          GITHUB_USERNAME: ${{ secrets.GITHUB_USERNAME }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        host: ${{ secrets.HOST }}
        username: ${{ secrets.USERNAME }}
        password: ${{ secrets.PASSWORD }}
        port: ${{ secrets.PORT }}
        envs: GITHUB_USERNAME, GITHUB_TOKEN
        script: |
          docker stop $(docker ps -a -q)
          docker login docker.pkg.github.com -u $GITHUB_USERNAME -p $GITHUB_TOKEN
          docker pull docker.pkg.github.com/my_github_username/my_repository_name/my_image_name:latest
          docker run -dit -p 80:80 docker.pkg.github.com/my_github_username/my_repository_name/my_image_name:latest

We previously published the app image to the Github Package Registry by signing in with the Github Credentials (GITHUB_USERNAME and GITHUB_TOKEN ). To pull the image from the registry we must login to archive so.

Let's commit and push our changes to master.

git add -A
git commit -m "deploy to digitalocean"
git push origin master

We're using the ssh-action to remotely access our droplet from our repository.

  • docker stop $(docker ps -a -q) stops all the previous running containers.
  • docker run -dit -p 80:80 my_github_username/my_repository_name/my_image_name:tag pull the lastest image and run it on port 80.

As you can see bellow the workflow is passing.

Alt workflow

Congratulations ๐ŸŽ‰! You can now access your react-app on your droplet IP_ADDRESS or DOMAIN_NAME.

Mine is running on http://167.172.51.225/

You can find the code in the react-with-actions repo. I also want you to keep in mind that pushing to master is a bad practice. I did it for this tutorial. You should create a branch and make a pull request. You can follow some of git best practices here.

Should you have any questions, please do not hesitate to reach out to me on kenessajr on twitter. Comment below if you have other feedback.

Discussion

pic
Editor guide
Collapse
christianmontero profile image
Christian Montero

I' m getting an error in the deploy stage, this is the output:

======CMD======
docker stop $(docker ps -a -q)
docker login docker.pkg.github.com -u $GITHUB_USERNAME -p $GITHUB_TOKEN
docker pull docker.pkg.github.com/fredomontero/bones/bones:latest
docker run -dit -p 3001:80 docker.pkg.github.com/fredomontero/bones/bones:latest
======END======
err: bash: line 2: docker: command not found
err: bash: line 2: docker: command not found
err: bash: line 3: docker: command not found
err: bash: line 4: docker: command not found

does anybody knows what' s wrong?

Collapse
kenessajr profile image
Remy Muhire Author

Hi Christian, can you check if you have docker installed on your production server?

Collapse
christianmontero profile image
Christian Montero

Hi Remy, thanks for answering I already solved, the issue was that I installed docker using snap, and reading on the web I realized that snap installs everything under /snap and that is not part of the environment variable PATH.

it was a little tricky because if I ssh from my local computer to the server and then run docker it works, but it didn't work from the yml file.

so instead of running docker I had to run /snap/bin/docker

or create a symlink:

sudo ln -s /snap/bin/docker /usr/bin/docker

Thank for this post bro it's very helpful!

Collapse
athmakuri profile image
Susheel Athmakuri

I am getting name unknown: The expected resource was not found. while publishing to the Github Package Registry.

LOG

Successfully built 831f744bab47
Successfully tagged docker.pkg.github.com/repo_name/image_name:latest
The push refers to repository [docker.pkg.github.com/repo_name/image_name]
d4c3bfb3f5d5: Preparing
3810cc0c140f: Preparing
3e207b409db3: Preparing
** name unknown: The expected resource was not found. **

DEPLOY FILE

name: build

on:
  push:
    branches: 
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js 13.10
      uses: actions/setup-node@v1
      with:
        node-version: '13.10'
    - name: Publish to Github Packages Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: github_username/repo_name/image_name
        registry: docker.pkg.github.com
        username: ${{ secrets.USERNAME_GITHUB }}
        password: ${{ secrets.githubTokenSecret }}
        dockerfile: Dockerfile-prod
Enter fullscreen mode Exit fullscreen mode

Not sure what the unknown thing is.

Collapse
kenessajr profile image
Remy Muhire Author

Hi @athmakuri ,

You're not specifying your Github username, repository name, and the Docker image name github_username/repo_name/image_name. For example, my Github username is kenessajr and my repository name and docker image are both called react-with-actions, kenessajr/react-with-actions/react-with-actions. What you have to do is editing and specifying correctly your variables.

Collapse
athmakuri profile image
Susheel Athmakuri

Hi Remy, Thanks for your response. Here is the actual name that I have, susheelv/arc_recipe/recipecreator for which I am getting the error.

Deploy File

    - name: Publish to Github Packages Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: susheelv/ARCRecipes/recipecreator
        registry: docker.pkg.github.com
        username: ${{ secrets.USERNAME_GITHUB }}
        password: ${{ secrets.githubTokenSecret }}
        dockerfile: Dockerfile-prod
        tags: latest

Error

Step 10/10 : CMD ["nginx", "-g", "daemon off;"]
 ---> Running in ec67f6ce496a
Removing intermediate container ec67f6ce496a
 ---> 3c2f28dbd026
Successfully built 3c2f28dbd026
Successfully tagged docker.pkg.github.com/***/arcrecipes/recipecreator:latest
The push refers to repository [docker.pkg.github.com/***/arcrecipes/recipecreator]
91f28809782e: Preparing
3810cc0c140f: Preparing
3e207b409db3: Preparing
name unknown: The expected resource was not found.
Thread Thread
athmakuri profile image
Susheel Athmakuri

Nevermind. I had to use the organization's name instead of my personal username to create a package.

Collapse
sadi304 profile image
Sadi Mahmud

Thanks for the article. I am having an issue here. Workflow is passed and done successfully but I have noticed the changes are not being reflected in digital ocean. Is this something related to caching of the docker package? Or are there any chances that docker package will not be updated with the latest code sometimes? Any dependencies related latest tag?

EDIT: have to use docker pull before running the docker.

Collapse
kenessajr profile image
Remy Muhire Author

Hello Sady, I just realized that I forget to add docker pull for pulling new images.

Collapse
galakhov profile image
Dmitry

Hey there, it's a nice deploy.yml file! One suggestion (from docker) is to use:
echo "$GITHUB_TOKEN" | docker login docker.pkg.github.com -u $GITHUB_USERNAME --password-stdin
instead of:
docker login docker.pkg.github.com -u $GITHUB_USERNAME -p $GITHUB_TOKEN
for the security reasons.

err: WARNING! Using -*** the CLI is insecure. Use --password-stdin.
err: WARNING! Your password will be stored unencrypted in /home/***/.docker/config.json.
err: Configure a credential helper to remove this warning. See
err: https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Enter fullscreen mode Exit fullscreen mode
Collapse
daveteu profile image
Dave

question: Wouldn't running Login + Pull first be a better choice to limit down time? And to prevent failure?

docker login docker.pkg.github.com -u $GITHUB_USERNAME -p $GITHUB_TOKEN
docker pull docker.pkg.github.com/my_github_username/my_repository_name/my_image_name:latest
docker stop $(docker ps -a -q)
docker run -dit -p 80:80 docker.pkg.github.com/my_github_username/my_repository_name/my_image_name:latest

Collapse
legkoletat profile image
Mikhail Kuznetcov

Next, let's add our GITHUB_USERNAME to the secrets. -

gives Failed to add secret. Name is invalid

Collapse
salgit profile image
Stephen Lightcap

Any token that starts with GITHUB is reserved by github and included in your build process already.

Collapse
arunkmrs profile image
Arun Kumar

Great article thanks for this. Could you also help with adding a certificate to our react app & use port https with it.

Collapse
kenessajr profile image
Remy Muhire Author

Yes definitely. I will try to update the tutorial ASAP.

Collapse
robertegan profile image
RobertEgan

Remy, this was very helpful. Thank you! Thank you! Thank you!

Collapse
reddyforcode profile image
ReddyTintaya

Use this in the Dockerfile

FROM node:alpine as build

instead of specifying the node version

if you have error

Collapse
napestershine profile image
napestershine

Hey

Thanks for this nice article. I have a question though. How can I do it without docker ?

Thanks