The author selected the Tech Education Fund to receive a donation as part of the Write for DOnations program.
When a developer is making continuous changes to an application, a deployment system with webhooks can streamline development, particularly for teams. Integrating Slack notifications for code changes into a team’s workflow can also be helpful if part of the team relies on back-end software like an API.
In this tutorial, you will build a React application with the create-react-app
npm
package. This package simplifies the work of bootstrapping a React project by transpiling syntax and streamlining work with dependencies and prerequisite tools. After adding your application code to a GitHub repository, you will configure Nginx to serve your updated project files. You will then download and set up the webhook server and configure GitHub to communicate with it when your code is modified. Finally, you’ll configure Slack to act as another webhook server, which will receive notifications when a successful deploy has been triggered.
Ultimately, the deployment system you are building in this article will look like this:
This short video shows an empty commit and push to the GitHub repository, which triggers the application build and notifications in Slack.
To complete this tutorial, you will need:
npm
installed on your local machine and server. For your server, follow the instructions on installing Node.js from a PPA in How To Install Node.js on Ubuntu 16.04. On your local machine, you can follow the project’s installation directions.Let’s first build the application that we will use to test our webhooks with create-react-app
. We can then create a GitHub repository and push the project code to it.
On your local machine, add the create-react-app
node module to your global repository and make the create-react-app
command available in your shell environment:
- sudo npm install -g create-react-app
Next, run create-react-app
to create a project called do-react-example-app
:
- create-react-app do-react-example-app
Navigate to the directory do-react-example-app
:
- cd do-react-example-app
With nano
or your favorite text editor, open the package.json
file:
- nano package.json
The file should look like this:
{
"name": "do-react-example-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-scripts": "1.0.17"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
The package.json
file includes the following scripts:
start
: This script is responsible for starting the development version of the application. It runs an HTTP server which serves the application.build
: This script is responsible for making the production version of the application. You will use this script on the server.test
: This script runs the default tests associated with the project.eject
: This script is an advanced feature of the create-react-app
package. If the developer is not satisfied with the build environment the package offers, it is possible to ‘eject’ the application, which will produce options that are otherwise unavailable (including things like custom CSS transpilers and JS processing tools).Close the file when you are finished inspecting the code.
Next, let’s create a GitHub repository for the project. You can follow this tutorial on creating a GitHub repository for guidance. Take note of the repository’s origin (i.e. its GitHub URL).
Back in your do-react-example-app
directory, initialize the repository with git
:
- git init
Next, add the remote origin with your GitHub URL:
- git remote add origin your-github-url
Stage all of the files in the project directory:
- git add .
Commit them:
- git commit -m "initial commit"
And push them to repository:
- git push origin master
For more information on creating GitHub repositories and initializing existing applications with git
, see GitHub’s documentation.
Once we have finished the repository setup, we can move on to specifying configuration details on our server.
With the repository in place, it is now possible to pull the application code from GitHub and configure Nginx to serve the application.
Log into your server, go to your home directory, and clone your repository:
- cd ~
- git clone your-github-url
Go to the cloned project:
- cd do-react-example-app
To create a build directory within the project and files for Nginx to serve, you will will need to run the yarn build
command. This runs the build script for the project, creating the build directory. This folder includes, among other things, an index.html
file, a JavaScript file, and a CSS file. The yarn
command will download all of the required node modules for your project:
- yarn && yarn build
Next, let’s make a symlink in the /var/www/
directory to the ~/do-react-example-app
directory. This will keep the application in our home directory, while making it available to Nginx to serve from the /var/www
directory:
- sudo ln -s ~/do-react-example-app /var/www/do-react-example-app
Note that this links to the project directory rather than to the build directory, which changes more frequently. Creating this link can be particularly helpful in scenarios where you are deploying new versions of the application: by creating a link to a stable version, you simplify the process of swapping it out later, as you deploy additional versions. If something goes wrong, you can also revert to a previous version in the same way.
Some permissions should be set on the symlink so Nginx can serve it properly:
- sudo chmod -R 755 /var/www
Next, let’s configure an Nginx server block to serve the build directory. Make a new server configuration by typing:
- sudo nano /etc/nginx/sites-available/test-server
Copy the following configuration, replacing your_server_ip_or_domain
with your IP or domain (if applicable):
server {
listen 80;
root /var/www/do-react-example-app/build;
index index.html index.htm index.nginx-debian.html;
server_name your_server_ip_or_domain;
location / {
try_files $uri /index.html;
}
}
The directives in this file include:
listen
: The property that configures the server listening port.root
: The path to the folder from which Ngnix will serve files.index
: The file that the server tries to serve first. It will try to serve any of following files from the /var/www/do-react-example-app/build
directory: index.html
, index.htm
, index.nginx-debian.html
, with priority from first to last.server_name
: The server domain name or IP.Next, make a symlink in the sites-enabled
directory:
- sudo ln -s /etc/nginx/sites-available/test-server /etc/nginx/sites-enabled/test-server
This will tell Nginx to enable the server block configuration from the sites-available
folder.
Check if the configuration is valid:
- sudo nginx -t
Finally, restart Nginx to apply the new configuration:
- sudo systemctl restart nginx
With these configuration details in place, we can move on to configuring the webhook.
Webhooks are simple HTTP servers that have configurable endpoints called hooks. Upon receiving HTTP requests, webhook servers execute customizable code that adheres to a set of configurable rules. There are already many webhook servers integrated into applications across the internet, including Slack.
The most widely-used implementation of a webhook server is Webhook, written in Go. We will use this tool to set up our webhook server.
Make sure you are in your home directory on your server:
- cd ~
Then download the webhook
:
- wget https://github.com/adnanh/webhook/releases/download/2.6.6/webhook-linux-amd64.tar.gz
Extract it:
- tar -xvf webhook-linux-amd64.tar.gz
Make the binary available in your environment by moving it to /usr/local/bin
:
- sudo mv webhook-linux-amd64/webhook /usr/local/bin
Last, clean up the downloaded files:
- rm -rf webhook-linux-amd64*
Test the availability of webhook
in your environment by typing:
- webhook -version
The output should display the webhook
version:
Outputwebhook version 2.6.5
Next, let’s set up hooks
and scripts
folders in the /opt
directory, where files for third-party applications usually go. Since the /opt
directory is usually owned by root
, we can create directories with root privileges and then transfer ownership to the local $USER
.
First, create the directories:
- sudo mkdir /opt/scripts
- sudo mkdir /opt/hooks
Then transfer the ownership to your $USER
:
- sudo chown -R $USER:$USER /opt/scripts
- sudo chown -R $USER:$USER /opt/hooks
Next, let’s configure the webhook
server by creating a hooks.json
file. With nano
or your favorite editor, create the hooks.json
file in /opt/hooks
directory:
- nano /opt/hooks/hooks.json
For the webhook
to be triggered when GitHub sends HTTP requests, our file will need a JSON array of rules. These rules consist of the following properties:
{
"id": "",
"execute-command": "",
"command-working-directory": "",
"pass-arguments-to-command": [],
"trigger-rule": {}
}
Specifically, these rules define the following information:
id
: The name of the endpoint the webhook server will serve. We will call this redeploy-app
.execute-command
: The path to the script that will be executed when the hook is triggered. In our case, this will be the redeploy.sh
script located in /opt/scripts/redeploy.sh
.command-working-directory
: The working directory that will be used when executing the command. We will use /opt/scripts
because that is where redeploy.sh
is located.pass-arguments-to-command
: The parameters passed to the script from the HTTP request. We will pass a commit message, pusher name, and commit id from the payload of the HTTP request. This same information will also be included in your Slack messages.The /opt/hooks/hooks.json
file should include the following information:
[
{
"id": "redeploy-app",
"execute-command": "/opt/scripts/redeploy.sh",
"command-working-directory": "/opt/scripts",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.message"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "head_commit.id"
}
],
"trigger-rule": {}
}
]
The payload of the GitHub HTTP POST request includes the head_commit.message
, pusher.name
, and head_commit.id
properties. When a configured event (like PUSH) happens in your GitHub repository, GitHub will send a POST request with a JSON body containing information about the event. Some examples of those POST payloads can be found in the GitHub Event Types docs.
The last property in the configuration file is the trigger-rule
property, which tells the webhook server under which condition the hook will be triggered. If left empty, the hook will always be triggered. In our case, we will configure the hook to be triggered when GitHub sends a POST request to our webhook server. Specifically, it will only be triggered if the GitHub secret (denoted here as your-github-secret
) in the HTTP request matches the one in the rule, and the commit happened to the master
branch.
Add the following code to define the trigger-rule
, replacing your-github-secret
with a password of your choosing:
...
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha1",
"secret": "your-github-secret",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
In full, /opt/hooks/hooks.json
will look like this:
[
{
"id": "redeploy-app",
"execute-command": "/opt/scripts/redeploy.sh",
"command-working-directory": "/opt/scripts",
"pass-arguments-to-command":
[
{
"source": "payload",
"name": "head_commit.message"
},
{
"source": "payload",
"name": "pusher.name"
},
{
"source": "payload",
"name": "head_commit.id"
}
],
"trigger-rule":
{
"and":
[
{
"match":
{
"type": "payload-hash-sha1",
"secret": "your-github-secret",
"parameter":
{
"source": "header",
"name": "X-Hub-Signature"
}
}
},
{
"match":
{
"type": "value",
"value": "refs/heads/master",
"parameter":
{
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
One final configuration item to check is your server’s firewall settings. The webhook server will be listening on port 9000
. This means that if a firewall is running on the server, it will need to allow connections to this port. To see a list of your current firewall rules, type:
- sudo ufw status
If port 9000
is not included in the list, enable it:
- sudo ufw allow 9000
For more information about ufw
, see this introduction to ufw essentials.
Next, let’s set up our GitHub repository to send HTTP requests to this endpoint.
Let’s configure our GitHub repository to send HTTP requests when a commit to master happens:
http://your_server_ip:9000/hooks/redeploy-app
. If you have a domain name, you can use that in place of your_server_ip
. Note that the endpoint name matches the id
property in the hook definition. This is a detail of webhook implementations: all hooks defined in hooks.json
will appear in the URL as http://your_server_ip:9000/hooks/id
, where id
is the id
in hooks.json
file.your-github-secret
) that you set in the hooks.json
definition.Now when someone pushes a commit to your repository, GitHub will send a POST request with the payload containing information about the commit event. Among other useful properties, it will contain the properties we defined in the trigger rule, so our webhook server can check whether the POST request was valid. If it is, it will contain other info like pusher.name
.
The full list of properties sent with payload can be found on the GitHub Webhooks page.
At this point, we have pointed the webhook to the redeploy.sh
script, but we haven’t created the script itself. It will do the work of pulling the latest master branch from our repository, installing node modules, and executing the build command.
Create the script:
- nano /opt/scripts/redeploy.sh
First, let’s add a function to top of the script that will clean up any files it has created. We can also use this as a place to notify third party software like Slack if the redeploy didn’t go through successfully:
#!/bin/bash -e
function cleanup {
echo "Error occoured"
# !!Placeholder for Slack notification
}
trap cleanup ERR
This tells the bash
interpreter that if the script finished abruptly, it should run the code in the cleanup
function.
Next, extract the parameters that webhook
passes to the script when executing it:
...
commit_message=$1 # head_commit.message
pusher_name=$2 # pusher.name
commit_id=$3 # head_commit.id
# !!Placeholder for Slack notification
Notice that the order of the parameters corresponds to the pass-arguments-to-command
property from the hooks.json
file.
Last, let’s call the commands necessary for redeploying the application:
...
cd ~/do-react-example-app/
git pull origin master
yarn && yarn build
# !!Placeholder for Slack notification
The script in full will look like this:
#!/bin/bash -e
function cleanup {
echo "Error occoured"
# !!Placeholder for Slack notification
}
trap cleanup ERR
commit_message=$1 # head_commit.message
pusher_name=$2 # pusher.name
commit_id=$3 # head_commit.id
# !!Placeholder for Slack notification
cd ~/do-react-example-app/
git pull origin master
yarn && yarn build
# !!Placeholder for Slack notification
The script will go to the folder, pull the code from latest master branch, install fresh packages, and the build the production version of the application.
Notice the !!Placeholder for Slack notification
. This is a placeholder for the last step in this tutorial. Without notifications, there is no real way of knowing if the script executed properly.
Make the script executable so the hook can execute it:
- chmod +x /opt/scripts/redeploy.sh
Because Nginx is configured to serve files from /var/www/do-react-example-app/build
, when this script executes, the build directory will be updated and Nginx will automatically serve new files.
Now we are ready to test the configuration. Let’s run the webhook server:
- webhook -hooks /opt/hooks/hooks.json -verbose
The -hooks
parameter tells webhook
the location of the configuration file.
You will see this output:
Output[webhook] 2017/12/10 13:32:03 version 2.6.5 starting
[webhook] 2017/12/10 13:32:03 setting up os signal watcher
[webhook] 2017/12/10 13:32:03 attempting to load hooks from /opt/hooks/hooks.json
[webhook] 2017/12/10 13:32:03 os signal watcher ready
[webhook] 2017/12/10 13:32:03 found 1 hook(s) in file
[webhook] 2017/12/10 13:32:03 loaded: redeploy-app
[webhook] 2017/12/10 13:32:03 serving hooks on http://0.0.0.0:9000/hooks/{id}
This tells us that everything is loaded properly and that our server is now serving the hook redeploy-app
via the URL http://0.0.0.0:9000/hooks/redeploy-app
. This exposes a path or hook on the server that can be executed. If you now do a simple REST call (like GET) with this URL, nothing special will happen because the hook rules were not satisfied. If we want the hook to be triggered successfully, we must fulfill the trigger-rule
we defined in hooks.json
.
Let’s test this with an empty commit in the local project directory. Leaving your webhook server running, navigate back to your local machine and type the following:
- git commit --allow-empty -m "Trigger notification"
Push the commit to the master branch:
- git push origin master
You will see output like this on your server:
Output[webhook] 2018/06/14 20:05:55 [af35f1] incoming HTTP request from 192.30.252.36:49554
[webhook] 2018/06/14 20:05:55 [af35f1] redeploy-app got matched
[webhook] 2018/06/14 20:05:55 [af35f1] redeploy-app hook triggered successfully
[webhook] 2018/06/14 20:05:55 200 | 726.412µs | 203.0.113.0:9000 | POST /hooks/redeploy-app
[webhook] 2018/06/14 20:05:55 [af35f1] executing /opt/scripts/redeploy.sh (/opt/scripts/redeploy.sh) with arguments ["/opt/scripts/redeploy.sh" "Trigger notification" "sammy" "82438acbf82f04d96c53cd684f8523231a1716d2"] and environment [] using /opt/scripts as cwd
Let’s now add Slack notifications and look at what happens when the hook triggers a successful build with notifications.
To receive Slack notifications when your app is redeployed, you can modify the redeploy.sh
script to send HTTP requests to Slack. It’s also necessary to configure Slack to receive notifications from your server by enabling the Webhook Integration in the Slack configuration panel. Once you have a Webhook URL from Slack, you can add information about the Slack webhook server to your script.
To configure Slack, take the following steps:
After that, you will be presented with a screen displaying the Slack webhook settings. Make note of the Webhook URL, which is the endpoint generated by the Slack webhook server. When you are finished taking note of this URL and making any other changes, be sure to press the Save Settings button at the bottom of the page.
Return to your server and open the redeploy.sh
script:
- nano /opt/scripts/redeploy.sh
In the previous step, we left placeholders in the script for Slack notifications, denoted as !!Placeholder for Slack notification
. We will now replace these with curl
calls that make POST HTTP requests to the Slack webhook server. The Slack hook expects the JSON body, which it will then parse, displaying the appropriate notification in the channel.
Replace the !!Placeholder for slack notification
with the following curl
calls. Note that you will need to replace your_slack_webhook_url
with the Webhook URL you noted earlier:
#!/bin/bash -e
function cleanup {
echo "Error occoured"
curl -X POST -H 'Content-type: application/json' --data "{
\"text\": \"Error occoured while building app with changes from ${pusher_name} (${commit_id} -> ${commit_message})\",
\"username\": \"buildbot\",
\"icon_url\": \"https://i.imgur.com/JTq5At3.png\"
}" your_slack_webhook_url
}
trap cleanup ERR
commit_message=$1 # head_commit.message
pusher_name=$2 # pusher.name
commit_id=$3 # head_commit.id
curl -X POST -H 'Content-type: application/json' --data "{
\"text\": \"Started building app with changes from ${pusher_name} (${commit_id} -> ${commit_message})\",
\"username\": \"buildbot\",
\"icon_url\": \"https://i.imgur.com/JTq5At3.png\"
}" your_slack_webhook_url
cd ~/do-react-example-app/
git pull origin master
yarn && yarn build
curl -X POST -H 'Content-type: application/json' --data "{
\"text\": \"Build and deploy finished with changes from ${pusher_name} (${commit_id} -> ${commit_message})\",
\"username\": \"buildbot\",
\"icon_url\": \"https://i.imgur.com/JTq5At3.png\"
}" your_slack_webhook_url
We have replaced each placeholder with a slightly different curl
call:
More on Slack bots and integrations can be found in the Slack webhooks documentation.
Again, we can test our hook with an empty commit in the local project directory. Leaving the webhook server running, navigate back to this directory and create the empty commit:
- git commit --allow-empty -m "Trigger notification"
Push the commit to the master branch to trigger the build:
- git push origin master
The output, including build information, will look like this:
Output[webhook] 2018/06/14 20:09:55 [1a67a4] incoming HTTP request from 192.30.252.34:62900
[webhook] 2018/06/14 20:09:55 [1a67a4] redeploy-app got matched
[webhook] 2018/06/14 20:09:55 [1a67a4] redeploy-app hook triggered successfully
[webhook] 2018/06/14 20:09:55 200 | 462.533µs | 203.0.113.0:9000 | POST /hooks/redeploy-app
[webhook] 2018/06/14 20:09:55 [1a67a4] executing /opt/scripts/redeploy.sh (/opt/scripts/redeploy.sh) with arguments ["/opt/scripts/redeploy.sh" "Trigger notification" "sammy" "5415869a4f126ccf4bfcf2951bcded69230f85c2"] and environment [] using /opt/scripts as cwd
[webhook] 2018/06/14 20:10:05 [1a67a4] command output: % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 228 0 2 100 226 11 1324 --:--:-- --:--:-- --:--:-- 1329
okFrom https://github.com/sammy/do-react-example-app
* branch master -> FETCH_HEAD
82438ac..5415869 master -> origin/master
Updating 82438ac..5415869
Fast-forward
yarn install v1.7.0
[1/4] Resolving packages...
success Already up-to-date.
Done in 1.16s.
yarn run v1.7.0
$ react-scripts build
Creating an optimized production build...
Compiled successfully.
File sizes after gzip:
36.94 KB build/static/js/main.a0b7d8d3.js
299 B build/static/css/main.c17080f1.css
The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:
"homepage" : "http://myname.github.io/myapp",
The build folder is ready to be deployed.
You may serve it with a static server:
yarn global add serve
serve -s build
Find out more about deployment here:
http://bit.ly/2vY88Kr
Done in 7.72s.
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 233 0 2 100 231 10 1165 --:--:-- --:--:-- --:--:-- 1166
ok
[webhook] 2018/06/14 20:10:05 [1a67a4] finished handling redeploy-app
In Slack, you will receive messages to your channel of choice notifying you that the application build has started and when it has finished.
We have now finished setting up a deployment system using webhooks, Nginx, shell scripts, and Slack. You should now be able to:
The system from this tutorial can be expanded, since the webhook server is modular and can be configured to work with other applications such as GitLab. If configuring the webhook server through JSON is too much, you can build a similar setup using Hookdoo. More information on how to configure trigger rules for webhook
can be found on the webhook project example hooks page.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
REally Nice Of You
worse a try
does the same format apply for gitlab? ie the hooks.json
Nice work!
How to keep webhook server running in the background? Or have I missed something ?
this is good for digitalocean