Permanent link to this article: github.com/HaoChuan942…

preface

A simple development and deployment process for a front-end project usually goes something like this: develop locally, build and package when a feature is complete, and then upload the generated static files to the server via FTP or other means. Push the code to a remote repository such as GitHub or the code cloud for hosting (for the purpose of this article, testing is not considered). This kind of workflow can be a bit taxing, and the frequent packing and uploading takes up a lot of time each day.

Ideally, you just create a “script” on the server, execute it, and it automatically pulls your project code from the Git server, starts your project, and every time you push your code to the Git server, it automatically pulls the latest code and recompiles it, updating the service.

To achieve the “ideal way” described above, this article details how to automate the deployment of a Vue SSR project using Nuxt + Webhooks + Docker. But first we need to solve a few problems:

  1. How to safely pull private repository code on the server?
  2. If the production environment (production) Start your project?
  3. What if you “notify” the server that your code has been updated?
  4. What if the project is automatically rebuilt without stopping the service, automatically updated?

To solve the above problems, you need to know the basics:

  1. SSH Key.
  2. The basicNuxt + DockerKnowledge.
  3. To understandWebhooks.
  4. The basicNode + expressKnowledge.

If you don’t know much about the above or how to combine them to achieve the “ideal way,” the next steps will take you from project creation to actual deployment.

A, use,create-nuxt-appScaffold creation project

The following figure shows the options for creating the Server framework. You can choose according to the actual situation of your project, but select Express as the server framework. This article will also introduce Express as the server framework.

Modify package.json NPM scripts

Nuxt scaffold-generated projects need to execute the NPM run build code first, and then execute the NPM start service in the production environment by default. This is a bit cumbersome, and is not suitable for automatic deployment, reconstruction, etc. Here, the two functions are combined into one, execute NPM start. You can use build and start services in your code. NODE_ENV (new Builder(Nuxt).build())) The build behavior of Nuxt will be different and optimized accordingly. In the production environment, nodemon is used instead of node command to start the service, which is used to monitor the changes of the server/index.js file and automatically restart the service when the server/index.js file is updated. NPM scripts before and after adjustment are as follows:

/ / before
"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server"."build": "nuxt build"."start": "cross-env NODE_ENV=production node server/index.js"
}
Copy the code
/ / after"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server"."start": "cross-env NODE_ENV=production nodemon server/index.js --watch server"
}
Copy the code

At the same time, delete the original conditional judgment in server/index.js:

//if (config.dev) {
  const builder = new Builder(nuxt);
  await builder.build();
/ /}
Copy the code

After tuning, executing NPM run dev starts a development service on port 3000 with code hot replacement (HMR) and other features, and executing NPM start builds the compressed code, And start a Production service with features such as gzip compression.

Add Webhooks interface

What are Webhooks? To put it simply: If you add webhooks to a repository, when you push your code, the Git server automatically sends a post request with an update payload to the address you specify. To learn more, read the GitHub introduction to Webhooks or the code cloud documentation. Since we use Express to create HTTP services, we can easily add an interface to receive POST requests from git servers like this:

.// Subscribe to Webhooks requests from git server (post type)
app.post('/webhooks'.function(req, res) {
  / / use secret token calls to the API authentication, detailed documentation: https://developer.github.com/webhooks/securing/
  const SECRET_TOKEN = 'b65c19b95906e027c5d8';
  // Calculate the signature
  const signature = `sha1=${crypto
    .createHmac('sha1', SECRET_TOKEN)
    .update(JSON.stringify(req.body))
    .digest('hex')}`;
  // Verify that the signature matches the signature in the Webhooks request
  const isValid = signature === req.headers['x-hub-signature'];
  // If the validation is successful, return the status and update the service
  if (isValid) {
    res.status(200).end('Authorized');
    upgrade();
  } else {
    // No permission message is displayed when authentication fails
    res.status(403).send('Permission Denied'); }}); .Copy the code

The app here is an Express application. We use Node’s Crypto module to calculate the signature and compare it to the signature in Webhooks request for authentication. To keep interface calls safe (the request body req.body that gets Webhooks requests here is due to body-Parser middleware). If the authentication succeeds, the system returns the success status and executes the upgrade function to update the service. If the authentication fails, the system returns no permission message. In the meantime, you need to add webhooks to the repository, as shown below:

How to seamlessly update the service

If your project has been successfully launched at http://www.example.com/, your interface will receive a Post request from GitHub every time you push code to the GitHub repository, After authentication is passed, the upgrade function is executed to update the service. For how to start the project on the server, let’s press the “upgrade” TAB first and see what the upgrade function does.

/** * Pull the latest code from git server, update NPM dependencies, and rebuild the project */
function upgrade() {
  execCommand('git pull -f && npm install'.true);
}
Copy the code

ExecCommand uses Node’s child_process module to create child processes that execute pull code, update NPM dependencies, and so on:

const { execSync } = require('child_process');
/** * Create child process, Run the command * @param {String} command Command to be executed * @param {Boolean} reBuild whether to reBuild the application * @param {Function} callback Callback after running the command */
function execCommand(command, reBuild, callback) {
  command && execSync(command, { stdio: [0.1.2] }, callback);
  // Rebuild the project based on the configuration file
  reBuild && build();
}
Copy the code

The build function reconstructs the project based on the configuration file. In this case, the upgrade flag indicates whether the application is upgrading.

/** * Build the project according to the configuration */
async function build() {
  if (upgrading) {
    return;
  }
  upgrading = true;
  // Import the nuxt.js parameter
  let config = require('.. /nuxt.config.js');
  // Set the value of config.dev according to the environment variable NODE_ENVconfig.dev = ! (process.env.NODE_ENV ==='production');
  // Initialize nuxt.js
  const nuxt = new Nuxt(config);
  // Build the application. Thanks to the environment variable NODE_ENV, this build behaves differently in development and production environments
  const builder = new Builder(nuxt);
  // Wait for the build
  await builder.build();
  // Update the Render middleware after the build is complete
  render = nuxt.render;
  // Reverse the flag
  upgrading = false;
  // If this is the first build, create HTTP server
  server || createServer();
}
Copy the code

The createServer function has two global variables, Render, which holds the newly built Nuxt. render middleware, and Server, which is the HTTP Server instance of the application.

/** * Create the application HTTP server */
function createServer() {
  // Add nuxT middleware to express applications. After the rebuild, the middleware will change
  // The advantage of this approach is that Express always uses the latest nuxt.render
  app.use(function() {
    render.apply(this.arguments);
  });
  // Start the service
  server = app.listen(port, function(error) {
    if (error) {
      return;
    }
    consola.ready({
      message: `Server listening on http://localhost:${port}`.badge: true
    });
  });
}
Copy the code

Visit here to see the complete server/index.js file. Nuxt will delete the files generated in the previous build (empty the.nuxt/dist/client and.nuxt/dist/server folders), and the new files will be generated after the build is completed. So what if the user happens to visit the site during that gap? One solution is to intervene in webpack this behavior, not to empty the two folders, but I found there is no Nuxt can modify this configuration in place (welcome comments), another solution is in the project to build, to give the user returns to a friendly prompt page, tell him in the system is upgrading. This is why I set the upgrading variable to indicate whether the application is upgrading. The following code shows how this effect can be achieved:

const express = require('express');
const app = express();
// Intercept all GET requests and return to the prompt page if the system is being upgraded
app.get(The '*'.function(req, res, next) {
  if (upgrading) {
    res.sendFile('./upgrading.html', { root: __dirname });
  } else{ next(); }});Copy the code

App.get (‘*’,…) It has to come first, and you can find the explanation in the Description here. This way, when the user visits the site just as the application is being rebuilt, a friendly notification page appears, and when the user visits the site again after the build is complete, the application is being upgraded. The server stays online the whole time, and the HTTP server does not stop or restart.

At this point, you are ready to upload the project code to GitHub or the code cloud (different providers may use Webhooks differently, you will need to refer to their documentation for a bit of authentication on the interface).

Deploy public key management

Add deployment public keys for private projects so that projects can be safely cloned and subsequently pulled for updates on the server or in Docker, see links 1 and 2. GitHub is used as an example.

  1. Generate a GitHub SSH key

    ssh-keygen -t rsa -C '[email protected]' -f ~/.ssh/github_id_rsa
    Copy the code

    Generally, you do not need to use -f ~/. SSH /github_id_rsa to specify the file name of the SSH Key. By default, id_rsa is generated. However, the generated SSH key names are customized to allow for the possibility of a machine using different Git servers. This is your Git server (GitHub) login email.

  2. SSH to create a config file and add the following information, refer to the documentation.

    # github
    Host github.com
    HostName github.com
    StrictHostKeyChecking no
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/github_id_rsa
    Copy the code

    Set Host and HostName to the git server domain name, IdentityFile to the private key path, and StrictHostKeyChecking to no to skip the query shown in the following figure. This is necessary for Docker to create images smoothly (otherwise you might want to write Expect scripts), You can also add the host keys to the known_hosts file by executing ssh-keyscan github.com > ~/. SSH /known_hosts.

  3. Add a deployment public key to the project repository

  4. Tests whether the public key is available

    ssh -T [email protected]
    Copy the code

    If something like the following appears, you are done and ready to go to the next step. 👏 👏 👏 🎉 🎉 🎉

So far, if you don’t need to use Docker deployment and use traditional deployment, you just need to install Node and Git on the server, clone the repository code to the server, and run NPM start to start the service on port 80. You can use nohup or forever to make the service live in the background.

Docker deployment

1. Install the Docker CE(Aliyun Ubuntu 18.04 has been tried)

2. Create Dockerfile

# Add node image, :8 is the specified node version, the default is to pull the latest
FROM node:8
# define SSH private key variables
ARG ssh_prv_key
SSH public key variable
ARG ssh_pub_key
# Create a folder called weblinks-nuxt-demo under /home
RUN mkdir -p /home/webhooks-nuxt-demo
# Specify workspace for RUN, CMD, etc
WORKDIR /home/webhooks-nuxt-demo
# create.ssh directory
RUN mkdir -p /root/.ssh
# Generate github_id_rsa, github_id_rsa.pub, and config files
RUN echo "$ssh_prv_key" > /root/.ssh/github_id_rsa && \
    echo "$ssh_pub_key" > /root/.ssh/github_id_rsa.pub && \
    echo "Host github.com\nHostName github.com\nStrictHostKeyChecking no\nPreferredAuthentications publickey\nIdentityFile /root/.ssh/github_id_rsa" > /root/.ssh/config
Change the user permission of the private key
RUN chmod 600 /root/.ssh/github_id_rsa
Clone the remote Git repository code into your workspace
RUN git clone [email protected]:HaoChuan9421/webhooks-nuxt-demo.git .
# install dependencies
RUN npm install
Expose 3000 ports
EXPOSE 3000
Execute script at startup
CMD npm start
Copy the code

3. Create a Docker Image

The contents of the previously created SSH public and private keys are read by the cat command and passed to the Docker as variables. Because git Clone and NPM install are required for the build process, it may take some time depending on machine performance and bandwidth. A normal build process looks like this:

docker build \
-t webhooks-nuxt-demo \
--build-arg ssh_prv_key="$(cat ~/.ssh/github_id_rsa)" \
--build-arg ssh_pub_key="$(cat ~/.ssh/github_id_rsa.pub)"\.Copy the code

4. Start the container

Start the container in the background and publish port 3000 in the container to port 80 on the host.

sudo docker run -d -p 80:3000 webhooks-nuxt-demo
Copy the code

5. Enter the executing container

If necessary, you can enter the container to perform some operations:

# list all containers
docker container ls -a
Enter the specified container
docker exec-i -t Specifies the container name or ID bashCopy the code

Seven, leave a back door

Sometimes we may need to execute commands to make the project more flexible, such as switching branches of the project, doing version rollback, and so on. If you need to connect to the server to access the container just to execute a single command, it will be a bit cumbersome.

// Reserve an interface for executing commands when necessary.
For example, upgrade the NPM package and rebuild the project by making the following AJAX request.
// var xhr = new XMLHttpRequest();
// xhr.open('post', '/command');
// xhr.setRequestHeader('access_token', 'b65c19b95906e027c5d8');
// xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
// xhr.send(
// JSON.stringify({
// command: 'npm update',
// reBuild: true
/ /})
// );
app.post('/command'.function(req, res) {
  // More stringent authentication can be implemented if necessary. This is just a demonstration
  if (req.headers['access_token'= = ='b65c19b95906e027c5d8') {
    // Execute the command and return the command execution result
    execCommand(req.body.command, req.body.reBuild, function(error, stdout, stderr) {
      if (error) {
        res.status(500).send(error);
      } else {
        res.status(200).json({ stdout, stderr }); }});// If it is a pure rebuild, there is no command to execute and the request is terminated without waiting for the result of the command execution
    if(! req.body.command && req.body.reBuild) { res.status(200).end('Authorized and rebuilding! '); }}else {
    res.status(403).send('Permission Denied'); }});Copy the code

Eight, summary

If you have successfully deployed your Vue SSR project by following the steps above, then every time you push your code to a Git server, it will automatically pull and update. 👏 👏 👏 🎉 🎉 🎉

Although I tried to cover in detail how to implement a front-end project for automated deployment, this may not be enough for a real project.

For example, for testing, maybe we need to create two Docker images (or use two servers), one on port 80 and one on port 3000, and pull the code for the master and dev branches respectively. By checking the payload of Webhooks, we decide which service should be updated for this push. Usually, we commit frequently on Dev. After the testers pass the test, we periodically merge the dev code into the Master branch. For the official version of the update.

For example, the improvement of log monitoring and so on, so THIS blog is a brick to welcome, welcome you to point out the shortcomings, comments and exchanges, or to submit PR to my project, we work together to improve this matter.