Selects logo a praymentis.

I AM SELECT

Feb 10
2021
For deployment automation there is not a single solution, but setting up an automated deployment is not hard and mostly follows the same steps.

How to automate the deployment of a web project

You finished a first presentable version of your web project with React / Vue / PHP / Ruby / … on your computer and you want the world to see it. You look into the documentation of your framework and you are lucky enough to find a deployment section. It tells you how to set up your server to run and serve your project. But wait … how does your code get to the server? How do you restart your server every time you update? Do you really need to upload your compiled project every time with Scp / Ftp client, then log into the server pressing the "refresh button"?

Over the years I learned how to deploy several web projects and I found that most deployments are similar and can be easily automated with a script. This script (you can see an example below) saves me a lot of time since it is integrated in my build system allowing me to deploy my project with one command:

npm run deploy

Deployment Steps

There is not a single solution for every deployment automation, there is not "the" software that does it for you, but most deployments, in my experience, follow these 4 steps

  • building the project
  • copying the build files to a server
  • installing or updating dependencies on the server
  • reloading the server

Example deployment script

In this example I use my latest Nuxt project and deploy it to a vserver with the domain example.org. The deployment in this example is not ideal but I liked it, since it shows nicely how to automate even complicated sets of commands.

deploy.sh
#!/usr/bin/env bash
npm run build
rsync -aRu --progress ecosystem.config.js package.json package-lock.json nuxt.config.js .nuxt/ static/ example.org:./my-project
ssh -t example.org "sudo cp -r /home/foo/my-project /opt; sudo chown -R pm2-user:pm2-user /opt/my-project; sudo -H -u pm2-user bash -c 'cd /opt/my-project; npm ci; pm2 restart my-project'"

The dissection

The script is initialized as a bash script and the build job is started on the local machine.

#!/usr/bin/env bash
npm run build

The build content is uploaded to the remote server using rsync. Using rsync compared to scp has the advantage that already uploaded files can be skipped, reducing the deployment time. In addition to that, not all files need to be uploaded so I chose only the relevant files and folders to be uploaded.

rsync -aRu --progress ecosystem.config.js package.json \
package-lock.json nuxt.config.js .nuxt/ static/ \
example.org:./my-project

The flags -aRu tell rsync to use the archive mode a (a shortcut for recursive upload, copy links, preserve timestamps and other meta data), R to use a relative directory path on the remote server (otherwise the full path is created), u to update files if they have newer timestamps but skip files that did not change, --progress to show the progress during the upload.

The next command is a bit more tricky, here is where the magic happens.

ssh -t myuser@example.org "\
    sudo cp -r /home/myuser/my-project /opt; \
    sudo chown -R pm2-user:pm2-user /opt/my-project; \
    sudo -H -u pm2-user bash -c '\
        cd /opt/my-project; \
        npm ci; \
        pm2 restart my-project\
     '\
 "

ssh -t will execute commands on the remote server example.org e.g. ssh -t example.org "echo 'hello world'" will run echo 'hello world' on example.org. With this I overcome a challenge created by the previous rsync command. In the previous command I was not able to copy the files directly into my destination directory (/opt/my-project), since the destination belongs to a different user (the pm2-user user which only has minimal rights on the server). I use the sudo cp -r command to copy the files to the actual destination directory. By executing sudo a user input will ask for the password of myuser on the local shell. Once the correct password is entered the content of /home/myuser/my-project is copied to /opt/my-project as root user. The copied files in /opt/my-project will belong to the root user. Since they should belong to the pm2-user user in the end, I change the ownership (chown) to the pm2-user user and group. In the next step I switch to the pm2-user user and execute several commands as pm2-user (sudo -H -u pm2-user bash -c 'command as pm2-user'). These commands install the dependencies of the project (npm ci) and restart the server.

Running all these commands on the remote server requires only one interaction: entering the password of the remote user. Switching users and executing commands as a different user than the one I initially logged in with, is pretty neat trick in my opinion. It shows that every action you usually do on a remote server shell can be automated.

How to get there

I usually do not start off with such a deployment script, but learn about and run each command at least once by typing it into a shell. With every new technology, new commands, and new ways of deployment are introduced that I learn and try out. Once the deployment was successful, I visit the command history on my local and remote machine and copy the commands into the script.

history

Extending the local build system

If you run a node project, you already have a build system available: npm. It's simple to integrate the automatic deployment script into npm. I extend the build system to have one entry point for building, testing, …, and deploying the project. Then I can come back to my project at any later time point, without having to remember the details of deploying this particular project.

package.json
{
        scripts: {
                …
                deploy: './scripts/deploy.sh'
        }
}

Now I can deploy my project by running

npm run deploy

Your project

While this example shows a specific use case with a Node project that uses Nuxt and Pm2, I hope I convinced you that this method will work for any (web) project that can be build and deployed using the command line.

Spend more time developing your project, and less on repetitive tasks. Happy automating!

Thanks to Olivier for proofreading.

Discussion

Let me know if you have feedback for this article on Mastodon.