DEV Community

Christian Montero
Christian Montero

Posted on

GitLab CI/CD example with a dockerized ReactJS App πŸš€

Good afternoon!
Today we'll be creating a CI/CD pipeline using GitLab to automate a dockerized ReactJS deployment πŸš€

Introduction

So today We're going to use Create-react-app in order to generate a simple ReactJS project, then we are going to dockerized that project in our local environment just to test it, Then we are going to upload our code to a GitLab repository in order to use it's CI/CD pipeline functionality and then deploy our dockerized app into a Digital Ocean droplet.

So, to follow this tutorial you should have:

1.- create-react-app installed βš›οΈ
2.- docker installed 🐳
3.- Good understanding about docker 🐳
4.- Good understanding about nginx πŸ†–
5.- GitLab account 🦊
6.- Digital Ocean account 🌊

Let's get started πŸ’ͺ

1.- Let's generate a react project using create-react-app

I'm gonna create a project called Budgefy πŸ– (an old project that I never finished), we just need to type:

npx create-react-app budgefy
Enter fullscreen mode Exit fullscreen mode

and we'll see something like this:
console output

After the project was successfully created, let' s verify that we can start the project typing this:

cd budgefy
npm start
Enter fullscreen mode Exit fullscreen mode

And it will open a new tab in our browser with the project running, you'll see this:

console output

Let's check if the tests are passing as well, by typing this:
(first ctrl + c to stop the project)

npm test
Enter fullscreen mode Exit fullscreen mode

and it will prompt this in the console:

Test Menu

and then just type 'a' to run all tests, and we expect this output:

Test output

2.- Let's dockerize our application

This is not an article about docker, so I'm assuming that you have a good understanding of docker, I'm planning to write an article about docker in a couple of days or maybe weeks, I'll do it as soon as possible. Anyways this is our docker file (this file will be in the root folder of our project):

# Set the base image to node:12-alpine
FROM node:12-alpine as build

# Specify where our app will live in the container
WORKDIR /app

# Copy the React App to the container
COPY . /app/

# Prepare the container for building React
RUN npm install
RUN npm install react-scripts@3.0.1 -g
# We want the production version
RUN npm run build

# Prepare nginx
FROM nginx:1.16.0-alpine
COPY --from=build /app/build /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d

# Fire up nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

We need to create a .dockerignore file (this file will be in the root folder of our project) to ignore the node_modules folder in our dockerized app, so, the content of our .dockerignore is this:

node_modules
Enter fullscreen mode Exit fullscreen mode

Also, since we will be using nginx (I will write about nginx in another article) we need to create the nginx folder in the root folder of our application, and inside we need to create the nginx.conf file with this content:

server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  error_page 500 502 503 504 /50x.html;

  location = /50x.html {
      root /usr/share/nginx/html;
  }

}

Enter fullscreen mode Exit fullscreen mode

Now that we have our files in place, make sure that in your terminal you are in the same folder where the Dockerfile is and let's run this command to create our image:

docker build --tag budgefy:1.0 .
Enter fullscreen mode Exit fullscreen mode

docker will log a lot of messages during the build process and at the end we can verify that our image was created by typing docker images and we should see our budgefy image, like this:

Docker images

and now we can run our image with this command:

docker run -p 4250:80 -d --name bugefycontainer budgefy:1.0
Enter fullscreen mode Exit fullscreen mode

After running our image, we will see an output like this one, where we'll see that we have a container running with our application

Docker images

so now, if you are using ubuntu, you can go to localhost:4250 and you will see our dockerized app running, in my case since I' m using Docker in windows, I have to access to the app through an IP that docker provides me, and this is our result:

Dockerized app running

Great everything is working!!!😎πŸ’ͺ

What's next? Let's upload our code to GitLab!

3.- Creating a project on GitLab 🦊

To create a Project on GitLab it's super easy, just login into your account and click on the "New Project" Button:

Create New Project

then just fill the name field, let's leave it as a private repository and click on "Create Project":

Create new Project on GitLab

Great! we have our project, let's upload our code, in our GitLab we'll see the instructions, in my case I need to follow this instructions:

Upload our code to GitLab

And after following those instructions we will see our code in our GitLab repository as you can see in this image:

Updated repository

4.- Let's create our Pipeline

In order to create our pipeline we need to add a new file in the root folder of our project whit the name: .gitlab-ci.yml

Once we added the .gitlab-ci.yml file and push it to our GitLab repository, GitLab will detect this file and a GitLab runner will go through the file and run all the jobs that we specify there. By default GitLab provides us with "shared runners" that will run the pipeline automatically unless we specify something else in our file. We can also use "specific runner" which basically means to install the GitLab runner service on a machine that allows you to customize your runner as you need, but for this scenario, we will use the shared runners.

In this file we can define the scripts that we want to run, we can run commands in sequence or in parallel, we can define where we want to deploy our app and specify whether we want to run the scripts automatically or trigger any of them manually.

We need to organize our scripts in a sequence that suits our application and in accordance with the test we want to perform

Let's see the next example:

stages:
  - build
  - test

build:
  stage: build
  image: node
  script: 
    - echo "Start building App"
    - npm install
    - npm build
    - echo "Build successfully!"

test:
  stage: test
  image: node
  script:
    - echo "Testing App"
    - npm install
    - CI=true npm test
    - echo "Test successfully!"

Enter fullscreen mode Exit fullscreen mode

let's include this code in our .gitlab-ci.yml file and commit those changes to our repo.

If we go to our repo we will see that our pipeline is running, let's take a look to our pipeline, we need to go to CI/CD and then to pipelines in our sidebar:

Sidebar menu

and then click in our status button:

Pipeline

then we will see the progress/status of our jobs as you can see here:

Jobs status

And since we test our App locally, everything should work as expected, and eventually we will see the successful message.

So, this was a very simple example to see how the pipeline works, we have two stages, and in the first one we just build the application and in the second one we run our tests. You might be asking you why are we running "npm install" 2 times, surely there's a better way to do it.

This is because each job runs in a new empty instance and we don't have any data from previous jobs, in order to share data we need to use artifacts or cache, what's the difference?

Artifacts:

1.- I usually the output of a build tool.
2.- In GitLab CI, are designed to save some compiled/generated paths of the build.
3.- Artifacts can be used to pass data between stages/jobs.

Cache:

1.- Caches are not to be used to store build results
2.- Cache should only be used as a temporary storage for project dependencies.

So, let's improve our pipeline:

stages:
  - build
  - test

build:
  stage: build
  image: node
  script: 
    - echo "Start building App"
    - npm install
    - npm build
    - echo "Build successfully!"
    artifacts:
      expire_in: 1 hour
      paths:
        - build
        - node_modules/

test:
  stage: test
  image: node
  script:
    - echo "Testing App"
    - CI=true npm test
    - echo "Test successfully!"
Enter fullscreen mode Exit fullscreen mode

Let's commit our code, and we'll see that everything it's still working, thats good! 🌟

5.- Let's build our image in the Pipeline

Now let's create another stage to dockerize our App. Take a look in our "docker-build" stage, our file will look like this:

stages:
  - build
  - test
  - docker-build

build:
  stage: build
  image: node
  script: 
    - echo "Start building App"
    - npm install
    - npm build
    - echo "Build successfully!"
  artifacts:
    expire_in: 1 hour
    paths:
      - build
      - node_modules/

test:
  stage: test
  image: node
  script:
    - echo "Testing App"
    - CI=true npm test
    - echo "Test successfully!"

docker-build:
  stage: docker-build
  image: docker:latest
  services: 
    - name: docker:19.03.8-dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE" .
    - docker push "$CI_REGISTRY_IMAGE"


Enter fullscreen mode Exit fullscreen mode

After committing and pushing our code, it will take a few minutes for the pipeline to finish the jobs, and if everything goes well, you'll see that all jobs passed, like this:

All Jobs passed

Also if you go to our sidebar in the GitLab dashboard, to "Packages and Registries" then to "Container Registry"

Packages and Registries

You will see the image that we just built 😎

docker image

Amazing job! πŸ‘Œ

So, what is happening in our "docker-build" stage? 🐳
Basically the same that we did in our local environment to build our docker image, we are using a docker image for this because we will need to run some docker commands, also we need to use the docker-dind service, in this case I'm using this specific version (docker:19.03.8-dind) because I had a couple of problems with other versions, and after that we are just login in to our GitLab account and build and push the image to the GitLab registry.

Also we are using some predefined GitLab variables, what is that?

Predefined Environment Variables:

GitLab offers a set of predefined variables that we can see and use if some of them are useful for our particular needs, you can see the full list here (https://docs.gitlab.com/ee/ci/variables/predefined_variables.html) In our particular case we are using this ones:

1.- CI_REGISTRY_USER: The username to use to push containers to the GitLab Container Registry, for the current project. 🀡

2.- CI_REGISTRY_PASSWORD: The password to use to push containers to the GitLab Container Registry, for the current project. πŸ™ˆ

3.- CI_REGISTRY: If the Container Registry is enabled it returns the address of GitLab’s Container Registry. This variable includes a :port value if one has been specified in the registry configuration. πŸ”—

4.- CI_REGISTRY_IMAGE: If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project πŸ”—

So, what's next? We need to deploy our App to our server!!! so first, let's

6.- Adding the Deploy stage πŸ”¨

Again we need to do what we did in our local environment, we need to pull our image from the GitLab registry and then we need to run it, and that's it! our App will be available in our server. So first let's add some commands to our .gitlab-ci.yml file, our last version of this file will be this one:

stages:
  - build
  - test
  - docker-build
  - deploy

build:
  stage: build
  image: node
  script: 
    - echo "Start building App"
    - npm install
    - npm build
    - echo "Build successfully!"
  artifacts:
    expire_in: 1 hour
    paths:
      - build
      - node_modules/

test:
  stage: test
  image: node
  script:
    - echo "Testing App"
    - CI=true npm test
    - echo "Test successfully!"

docker-build:
  stage: docker-build
  image: docker:latest
  services: 
    - name: docker:19.03.8-dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE" .
    - docker push "$CI_REGISTRY_IMAGE"
    - echo "Registry image:" $CI_REGISTRY_IMAGE

deploy:
  stage: deploy
  image: kroniak/ssh-client
  before_script:
    - echo "deploying app"
  script:
    - chmod 400 $SSH_PRIVATE_KEY
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY root@$PROD_SERVER_IP "docker pull registry.gitlab.com/alfredomartinezzz/budgefy"
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY root@$PROD_SERVER_IP "docker stop budgefycontainer || true && docker rm budgefycontainer || true"
    - ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY root@$PROD_SERVER_IP "docker run -p 3001:80 -d --name budgefycontainer registry.gitlab.com/alfredomartinezzz/budgefy"

Enter fullscreen mode Exit fullscreen mode

What are we doing?

In order to make this happens, we need to establish a ssh connection between our pipeline and our server, to do that we will need to store the IP of our server as a environment variable and also our private key.

So, for this stage we will use an image with a ssh client (kroniak/ssh-client) and we will run our commands 1 by 1 like this:

ssh -o StrictHostKeyChecking=no -i <private_key> <user_in_server>@<server_ip> "<command>"
Enter fullscreen mode Exit fullscreen mode

But if we want to test our last stage, we need to let our server ready!

Do not commit/push this changes (it will throw an error) we'll do it later

6.- Creating our server in Digital Ocean 🌊

You don't need to use Digital Ocean but I think that it's a very fast and easy option to get our server up an running! you just need to create an account, most of the time they give 100 dlls that you can use in the next 60 days, the server that we'll be using costs 5 dlls per month, so I found digital ocean very useful to practice and learn.

So just go ahead and create your account it will ask you for a payment method, you need to introduce your credit card but it won't charge you a cent.

Once you have your account, go to your dashboard and create a Droplet

Creating a Droplet

Then you need to choose your droplet requirements, we need a very basic one, choose the one of 5 dlls per month as you can see in this image:

Choosing our requirements

You can leave the rest of the options as they are, just need to type a password and give your server a cool name 😎

Creating our Droplet

And that's it, then it will take around 55 seconds to get your server up and running, pretty simple isn't? πŸ‘Œ

Now you can see your server and it's IP!

Our server

So now, let's get inside of our server via SSH from our local environment, let's go to our terminal (I'm using the cmder terminal for windows, if you are using the regular one, maybe you need to download putty or probably you can establish a ssh connection from the powershell, if you are on Mac or Linux you can do it from the regular terminal), so we just need to type:

ssh root@<server_ip>
Enter fullscreen mode Exit fullscreen mode

it will prompt you a message if you want to establish the connection:

Accessing to our server

and then it will ask you for the password that you established when you created your droplet, just type it in and then you'll be in!

You are in!

So now that we are in, we have a clean ubuntu server, we need to install docker, and let's login to our GitLab account, pull our project image and run it.

Here's a very simple guide to install docker in our ubuntu server: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04

We can verify that docker was successfully installed by typing docker -v or/and docker ps to list our containers:

Verifying docker installation

so, lets go to our Container Registry in GitLab, we will find a blue button that says "CLI commands":

CLI commands

We'll need the login into our GitLab account, and then we need to pull and run the image manually in our server, so let's do it.

Let's login:

login

Then let's pull our image:

Pulling our image

And then let's run it with this command, make sure that you change your image name if it's different and if you want to use another port, just change it, in my case I'll run it with this command:

docker run -p 3005:80 -d --name budgefycontainer registry.gitlab.com/alfredomartinezzz/budgefy
Enter fullscreen mode Exit fullscreen mode

running our image from the server

We can run the docker ps command to see our containers:

Listing our containers

And then let's go to our browser and go to our SERVER_IP:PORT

In my case I will access to the app on port 3005 and the IP of my server is: 138.68.254.184

And now we can see our App up and running in our server! as simple as that! πŸ‘

App running on server

So, now that we verify that our server is running perfectly and we can run our app there, we need to store our server's private key as an environment variable in our GitLab Project and also we need to store the IP address, so let's do it.

Let's go to our sidebar in our GitLab dashboard, and let's click on settings and then CI/CD we will see a lot of options, let's expand the variables section:

Settings

Then click on the "Add variable" button and a modal will pop up, our variable key will be "PROD_SERVER_IP" and the value will be our server IP, leave the rest of the options as they are and click on "Add variable".

Now we need to add our private key, but first let's create one in our server. Go to your server, open the terminal and type this:

ssh-keygen -m PEM -t rsa -b 4096 -C "your_email@example.com"
Enter fullscreen mode Exit fullscreen mode

it will ask you for a file to save the key, just type enter to use the default one, then it will ask you for a passphrase, for this example let's leave it empty and press enter a couple of times, and then you will see a successful message, then we need to copy our private key add it to our project on GitLab, we can run this command to see the our private key:

ssh key generation

then let's copy our private key

let's type cat ~/.ssh/id_rsa and copy the output, create a new variable, the key will be SSH_PRIVATE_KEY and the value will be our private key:

cat ~/.ssh/id_rsa
Enter fullscreen mode Exit fullscreen mode

so, let's copy the content and paste it.

Adding our ptivate key

Then we need to go to our server and run this command:

cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Now that everything is ready, let's commit and push our code to see the result.

Final result

That's all, now every time that we push our code into our repo, our pipeline will build our app, then it will run our tests, it will dockerize our app and push it into the GitLab Registry and finally it will deploy our App into our server!

I hope you enjoyed this post and found it useful, if you like it, feel free to share, also if you have any thoughts about this post, feel free to comment here or contact me, any feedback would be appreciated.

Have a nice day! ✌️

Latest comments (17)

Collapse
 
xsvato01 profile image
xsvato01

Thank you for the great tutorial! I just wonder how is the Dockerfile connected to the .gitlab-ci.yml. Why do you run npm build in the build stage of .gitlab-ci.yml when you does it already in Dockerfile?

Collapse
 
xpcrts profile image
Phok Chanrithisak • Edited

Reference link Gitlab Documentation: click here
After docker pull registry.gitlab.com/...

Output like this : Error response from daemon: pull access denied for registry.gitlab.com/..., repository does not exist or may require 'docker login': denied: requested access to the resource is denied

You need to login again using this command docker login registry.example.com -u <username_of_gitlab_account> -p <token> e.g: docker login registry.gitlab.com -u <username_of_gitlab_account> -p <token>

Token get from: Go to your gitlab profile icon > Select "Edit Profile" > on left hand side tab select "Access Tokens", then add any name on "Token name" Box for example: test-token, choose any of your "expiration date" > for "Select Scope", check all boxes > Then Click on box: "Create personal access token" > on top you will get token string,

come back to
docker login registry.example.com -u <username_of_gitlab_account> -p <token>
paste that token replace of your <token> and run on your server terminal.

Collapse
 
michaelandrianai profile image
MichaΓ«l Andrianaivomanana

Hello there! I got issue from

ssh -o StrictHostKeyChecking=no -i $SSH_PRIVATE_KEY root@$PROD_SERVER_IP "docker pull registery_private"

It's return access denied

Collapse
 
xpcrts profile image
Phok Chanrithisak

me too

Collapse
 
nickty profile image
nickty

It is expected that you will create local environment and have gitlab-yml setup for local environment. Then we could I deploy into server.
What is the use of locker docker here.
Where is Nginx working?

Please help?

Collapse
 
igorbuts profile image
Igor Buts • Edited

Thanks for a good and informative article. It worked for me.

Who follows the example, don't forget to make SSH_PRIVATE_KEY variable of Type: File (as it is shown on screenshot). I think it would be good to update the article and state it in the text explicitly, if possible.

Collapse
 
nickty profile image
nickty

Yes, you are right. It took my one day to solve.

Collapse
 
andreapigatto profile image
andreapigatto

Really nice! I think that to make more robust the production you could use docker swarm that offer nice features for example container restarting in case of down. But is more Docker thing and out of educational purpose of the post.

Collapse
 
alastrat profile image
AndrΓ©s Lastra • Edited

For some reason I'm getting this error.

I'm deploying to ec2, using de .pem key given by aws (passed as env var). But still, it doesn't work.

Collapse
 
duongnguyen1216 profile image
DΖ°Ζ‘ng Nguyα»…n • Edited

Hey, thank you very much for very detailed example.
I did it successfully and I found some point

  • The final .gitlab-ci.yml file, at deploy state, port should be 3005
  • $SSH_PRIVATE_KEY is string, not a file so we can't use chmod , I've replaced by this line
  stage: deploy
  image: kroniak/ssh-client
  before_script:
    - echo "deploying app"
  script:
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > key.pem
    - chmod 400 key.pem
    - ssh -o StrictHostKeyChecking=no -i key.pem root@$PROD_SERVER_IP ...
    - ssh -o StrictHostKeyChecking=no -i key.pem root@$PROD_SERVER_IP ...
    - ssh -o StrictHostKeyChecking=no -i key.pem root@$PROD_SERVER_IP ...
Enter fullscreen mode Exit fullscreen mode

I used EC2 Ubuntu 18.04 Server.

Thank you again <3

Collapse
 
nickty profile image
nickty

Please share with us the updated code

Collapse
 
tomasbalaz profile image
Tomas Balaz • Edited

You can set your private key gitlab variable as file and then you don't have to create key.pem file

Collapse
 
swetank01 profile image
swetank soni

I just got tricked with that error. Thanks for the fix.πŸ₯³

Collapse
 
christianmontero profile image
Christian Montero

oh! sorry for that mistake bro! πŸ˜”
but thanks for sharing your solution this will help other devs! πŸ’ͺ
and thank you for reading bro! πŸ‘

Collapse
 
crjhoan profile image
Jhoan Amado

Love it, it is very detailed

Collapse
 
christianmontero profile image
Christian Montero

thanks for reading and for the feedback πŸ‘

Collapse
 
aimeeca20205297 profile image
Aimee Camacho

πŸ‘πŸ‘πŸ‘πŸ‘