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
and we'll see something like this:
After the project was successfully created, let' s verify that we can start the project typing this:
cd budgefy
npm start
And it will open a new tab in our browser with the project running, you'll see this:
Let's check if the tests are passing as well, by typing this:
(first ctrl + c to stop the project)
npm test
and it will prompt this in the console:
and then just type 'a' to run all tests, and we expect this 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;"]
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
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;
}
}
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 .
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:
and now we can run our image with this command:
docker run -p 4250:80 -d --name bugefycontainer budgefy:1.0
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
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:
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:
then just fill the name field, let's leave it as a private repository and click on "Create Project":
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:
And after following those instructions we will see our code in our GitLab repository as you can see in this image:
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!"
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:
and then click in our status button:
then we will see the progress/status of our jobs as you can see here:
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!"
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"
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:
Also if you go to our sidebar in the GitLab dashboard, to "Packages and Registries" then to "Container Registry"
You will see the image that we just built 😎
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"
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>"
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
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:
You can leave the rest of the options as they are, just need to type a password and give your server a cool name 😎
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!
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>
it will prompt you a message if you want to establish the connection:
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!
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:
so, lets go to our Container Registry in GitLab, we will find a blue button that says "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:
Then let's pull 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
We can run the docker ps command to see 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! 👍
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:
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"
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:
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
so, let's copy the content and paste it.
Then we need to go to our server and run this command:
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
Now that everything is ready, let's commit and push our code to see the 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! ✌️
Top comments (17)
Hey, thank you very much for very detailed example.
I did it successfully and I found some point
.gitlab-ci.yml
file, at deploy state, port should be 3005chmod
, I've replaced by this lineI used EC2 Ubuntu 18.04 Server.
Thank you again <3
oh! sorry for that mistake bro! 😔
but thanks for sharing your solution this will help other devs! 💪
and thank you for reading bro! 👍
You can set your private key gitlab variable as file and then you don't have to create key.pem file
I just got tricked with that error. Thanks for the fix.🥳
Please share with us the updated code
Thanks for a good and informative article. It worked for me.
Who follows the example, don't forget to make
SSH_PRIVATE_KEY
variable ofType: 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.Yes, you are right. It took my one day to solve.
Love it, it is very detailed
thanks for reading and for the feedback 👍
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.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?
👍👍👍👍
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?
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.
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.
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
me too