{"id":1939,"date":"2025-01-28T17:03:46","date_gmt":"2025-01-28T15:03:46","guid":{"rendered":"https:\/\/upcloud.com\/global\/us\/resources\/tutorials\/deploying-a-node-js-application-to-upcloud-from-gitlab-ci-cd\/"},"modified":"2025-01-28T17:03:46","modified_gmt":"2025-01-28T15:03:46","slug":"deploying-a-node-js-application-to-upcloud-from-gitlab-ci-cd","status":"publish","type":"tutorial","link":"https:\/\/upcloud.com\/global\/resources\/tutorials\/deploying-a-node-js-application-to-upcloud-from-gitlab-ci-cd\/","title":{"rendered":"Deploying a Node.js Application to UpCloud From Gitlab CI\/CD"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/\" target=\"_blank\" rel=\"noopener\">GitLab CI\/CD<\/a> and <a href=\"https:\/\/upcloud.com\/global\/\">UpCloud<\/a> can be a powerful duo when it comes to deploying Node.js applications. GitLab CI\/CD automates the entire process of building, testing, and deploying your Node.js apps, making it easier than ever to push updates and ensure quality. Meanwhile, UpCloud offers a reliable and cost-effective cloud infrastructure that\u2019s perfect for hosting your applications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this tutorial, you\u2019ll learn how to set up your deployment pipeline from GitLab to UpCloud, creating a smoother, automated deployment workflow and achieving faster releases.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Why Deploy to UpCloud From GitLab CI\/CD?<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Deploying to UpCloud using GitLab CI\/CD can simplify DevOps for many developers. For small to medium-sized applications, it easily and quickly automates the deployment process, allowing you to focus on writing code rather than managing deployments. This means your web applications or APIs can receive smooth and consistent updates without the usual headaches of connecting to the server and manually updating and restarting your application. Plus, automated workflows significantly reduce deployment times and errors, ensuring that your application is always up-to-date.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Imagine a SaaS company rolling out microservices on UpCloud or a startup managing continuous delivery for its Node.js applications\u2014GitLab CI\/CD can help such teams forget about DevOps entirely and just focus on their app\u2019s development. Freelancers and small teams can also find it incredibly useful for deploying client projects quickly and reliably, freeing up time for more important tasks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When it comes to the infrastructure provider, UpCloud stands out with its high performance, customizable plans, and scalability. In this tutorial, you will get a glance at the various resource plans and the simple setup process that UpCloud offers to help you get your Node.js applications to production.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Overview and Prerequisites<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In this tutorial, you will first fork <a href=\"https:\/\/gitlab.com\/kharsh39\/random-facts-generator\" target=\"_blank\" rel=\"noopener\">this GitLab repository<\/a> with a simple Node.js application to your GitLab account. Then, you will set up an UpCloud server on which the app will be deployed. Once the server is ready, you will set up the Node.js app on it manually for the first time. Then, you will configure a GitLab CI\/CD pipeline in the forked GitLab repository to automatically update and redeploy the application whenever a new commit is pushed to it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To follow along, you will need the following:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u2013 A GitLab account. You will use this account to fork your copy of <a href=\"https:\/\/gitlab.com\/kharsh39\/random-facts-generator\" target=\"_blank\" rel=\"noopener\">this GitLab repository<\/a> on which you will set up the CI\/CD workflow. You will learn more about this repository shortly.<\/li>\n\n\n\n<li>\u2013 An UpCloud account. You can <a href=\"https:\/\/signup.upcloud.com\/\">sign up for a free trial here<\/a>.<\/li>\n\n\n\n<li>\u2013 Basic understanding of <a href=\"https:\/\/www.digitalocean.com\/community\/tutorials\/ssh-essentials-working-with-ssh-servers-clients-and-keys\" target=\"_blank\" rel=\"noopener\">SSH<\/a> and <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/yaml\/\" target=\"_blank\" rel=\"noopener\">GitLab CI\/CD YAML configuration<\/a>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have these in place, let\u2019s now take a quick look at the example app that you will deploy through the pipeline in this tutorial. The app in the repository is a random facts generator. It has a list of a few facts in the source code, and it picks and returns a fact from it randomly on each API call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\/\/ contents of index.js\nconst express = require('express');\nconst app = express();\nconst PORT = process.env.PORT || 5001;\n\n\/\/ Array of random facts\nconst facts = [\n    \"Honey never spoils.\",\n    \"A group of flamingos is called a 'flamboyance.'\",\n    \"Bananas are berries, but strawberries aren't.\",\n    \"Octopuses have three hearts.\",\n    \"Australia is believed to have lost to emus in the Great Emu War of 1932\",\n    \"A jiffy is an actual unit of time: 1\/100th of a second.\",\n    \"Cows have best friends and get stressed when they are separated.\",\n    \"The shortest war in history lasted 38 minutes.\",\n    \"The Eiffel Tower can be 15 cm taller during the summer.\",\n    \"A small child could swim through the veins of a blue whale.\"\n];\n\n\/\/ Serve static files (HTML, CSS, JS)\napp.use(express.static('public'));\n\n\/\/ Endpoint to get a random fact\napp.get('\/random-fact', (req, res) =&gt; {\n    const randomIndex = Math.floor(Math.random() * facts.length);\n    res.json({ fact: facts[randomIndex] });\n});\n\n\/\/ Start the server\napp.listen(PORT, () =&gt; {\n    console.log(`Server is running on http:\/\/localhost:${PORT}`);\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It also serves a static HTML page on its root path (\/). This page contains a heading, a button to get a random fact, a placeholder for the random fact to be displayed to the user, and a footer at the bottom of the page. Here\u2019s the source code for this page:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n    &lt;meta charset=\"UTF-8\"&gt;\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"&gt;\n    &lt;title&gt;Random Facts Generator&lt;\/title&gt;\n    &lt;style&gt;\n        body {\n            font-family: Arial, sans-serif;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            height: 98vh;\n            background-color: #f5f5f5;\n        }\n        button {\n            padding: 10px 20px;\n            font-size: 16px;\n            cursor: pointer;\n        }\n        p {\n            margin-top: 20px;\n            font-size: 20px;\n            text-align: center;\n        }\n\n        #footer {\n            position: absolute;\n            bottom: 10px;\n            font-size: 0.8rem;\n        }\n    &lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n\n&lt;h1&gt;Random Fact Generator&lt;\/h1&gt;\n&lt;button id=\"generate\"&gt;Get a Random Fact&lt;\/button&gt;\n&lt;p id=\"fact\"&gt;&lt;\/p&gt;\n&lt;p id=\"footer\"&gt;Made in 2024&lt;\/p&gt;\n\n&lt;script&gt;\n    document.getElementById('generate').onclick = async () =&gt; {\n        const response = await fetch('\/random-fact');\n        const data = await response.json();\n        document.getElementById('fact').innerText = data.fact;\n    };\n&lt;\/script&gt;\n\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can try running the app locally using the following commands:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\"># Clone the repo\ngit clone https:\/\/gitlab.com\/kharsh39\/random-facts-generator\ncd random-facts-generator\n\n# Install dependencies\nyarn\n\n# Run the development server\nyarn dev<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can now go to <code>http:\/\/localhost:5001<\/code> to view the app in action:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-52-1024x588.png\" alt=\"-\" class=\"wp-image-46203\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once you click the <strong>Get a Random Fact<\/strong> button, you will, indeed, get a random fact.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-53-1024x588.png\" alt=\"-\" class=\"wp-image-46204\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ensure that you fork a copy of this repo to your GitLab account before moving ahead. This is important because you will need to make changes to the source code of this app to trigger the CI\/CD pipeline and update the deployed application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now, let\u2019s move on to setting up the UpCloud server where you will deploy this application.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Setting up an UpCloud Server<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can use either the UpCloud web dashboard or the <code>upctl<\/code> CLI to set up an UpCloud web server. This section will walk you through both of these options. For beginners, it\u2019s best to explore the web dashboard at least once before you switch to the CLI so that you have a clear idea of all the resource options that UpCloud offers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Option 1: Through the Web Dashboard<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Head over to the <a href=\"https:\/\/hub.upcloud.com\/dashboard\">UpCloud web dashboard<\/a> and sign in with your UpCloud account. Once you log in, this is how your dashboard will look like:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-54-1024x588.png\" alt=\"-\" class=\"wp-image-46205\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Click on the <strong>+ Deploy new<\/strong> button on the top right of the page:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-55-1024x588.png\" alt=\"-\" class=\"wp-image-46206\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Select <strong>Server<\/strong> from the list that pops up:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-56-1024x588.png\" alt=\"-\" class=\"wp-image-46207\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will be taken to the <strong>Deploy a new server<\/strong> where you will configure the specifications of your server:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-57-1024x588.png\" alt=\"-\" class=\"wp-image-46208\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Server Location<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Start by choosing the physical location of your server. You should choose the location closest to you for the least latency when connecting to the server remotely. I\u2019ve chosen <strong>SG-SIN1<\/strong> as it is the closest to my location.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-58-1024x840.png\" alt=\"-\" class=\"wp-image-46209\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Server Plan<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Next up, you need to choose a server plan:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-59-1024x588.png\" alt=\"-\" class=\"wp-image-46210\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The server plan defines the CPU, RAM, and storage space available to your server. UpCloud provides you with a range of options from developer-focused small sizes for testing purposes and personal projects to large and cost-effective cloud native plans that unbundle storage and IPv4 addresses from the plans. You can learn more about the available plans <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/cloud-server-plans\/\">here<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since this is a test project, choose the first option in the <em>Developer<\/em> list (1 CPU core, 1 GB RAM, and 10 GB storage).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Storage<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Next, you can configure the storage available to your server. Since the plan you chose above already comes with 10 GB storage, you will see it pre-configured in the list:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-60-1024x588.png\" alt=\"-\" class=\"wp-image-46211\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You can choose to add more storage devices at this point. You can learn how to do that <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/adding-removing-storage-devices\/\">here<\/a>. For this tutorial, a 10 GB storage device is sufficient, so leave this section as it is and move ahead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Automated Backups<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Next, UpCloud offers you the option to set up automated backups of your server. Starting from the <em>General Purpose<\/em> server plans, the <em>Day plan<\/em>, which takes a backup every day and replaces the backup from the previous day, is included for free. You can choose to move up to the weekly, monthly, or yearly plans depending on your business needs.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-61-1024x588.png\" alt=\"-\" class=\"wp-image-46212\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For this tutorial, leave the automated backups disabled.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Operating System<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After the hardware configuration is done, it\u2019s time to choose the operating system for the server. UpCloud provides you with a few popular public templates to choose from along with the option of a wider variety of distributions under the CDROM tab and the ability to download and install nearly any other possible operating system using <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/importing-server-image\/\">custom images<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-62-1024x588.png\" alt=\"-\" class=\"wp-image-46213\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">There are lots of technical considerations, opinions, and personal\/project preferences that need to be taken into account before making this choice. However, for this tutorial, select <strong>AlmaLinux 9<\/strong> as it is a lightweight option compared to enterprise distributions of Linux.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Note: The commands used in this tutorial have been written with AlmaLinux in mind, so using a different OS might cause you to run into unexpected issues.<\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Network<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Next, you can configure the network settings of the server. All server plans include public IPv4 and IPv6 addresses and a private Utility Network connection by default. You also have the option to create <a href=\"https:\/\/upcloud.com\/global\/products\/software-defined-networking\/\">SDN private networks<\/a> and attach them to the server if needed.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-63-1024x588.png\" alt=\"-\" class=\"wp-image-46214\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You can leave this section as it is and move ahead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Optionals<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This section allows you to enable or disable the metadata service for your server and configure anti-affinity server groups.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-64-1024x588.png\" alt=\"-\" class=\"wp-image-46215\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/upcloud-metadata-service\/\">metadata service<\/a> is an API that provides information about the deployed cloud server to the server environment itself. This information is used when running cloud initialization scripts after the initial deployment. <a href=\"https:\/\/upcloud.com\/global\/docs\/products\/cloud-servers\/anti-affinity\/\">Anti-affinity server groups<\/a> allow servers to be arranged into groups that try to place their members on separate hosts to achieve high fault tolerance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since you\u2019ve selected AlmaLinux for this tutorial (and it is a cloud-init enabled template), the metadata service is enabled by default. You can skip the anti-affinity server group for now and move ahead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Login Method<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is an important part of the server setup. The login method section requires you to either configure an SSH key pair to authenticate with your server or set it to use one-time passwords.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">AlmaLinux does not support one-time passwords, so you need to <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/use-ssh-keys-authentication\/\">create an SSH key pair<\/a> and add its public key here by clicking on the <strong>+ Add new<\/strong> button<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-65-1024x588.png\" alt=\"-\" class=\"wp-image-46216\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">On MacOS and Windows environments, you will need the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Ssh-keygen\" target=\"_blank\" rel=\"noopener\">ssh-keygen<\/a> tool to create the key pair. Open up a terminal window and run the following command:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ssh-keygen -t rsa<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The tool will ask you to choose a location where the file will be saved:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Generating public\/private rsa key pair.<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Enter file in which to save the key (\/Users\/kumarharsh\/.ssh\/id_rsa):<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you do not want to overwrite your current <strong>~\/.ssh\/id_rsa<\/strong> key, provide a new file location, such as <code>.\/id_rsa_fact-gen_ci<\/code>, and press Enter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The tool will then ask you for a passphrase. It is recommended to not set a passphrase to avoid running into issues when setting up the CI to use these keys. Press Enter twice to complete the key generation. Here\u2019s what the output will look like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 ssh-keygen -t rsa\nGenerating public\/private rsa key pair.\nEnter file in which to save the key (\/Users\/kumarharsh\/.ssh\/id_rsa): id_rsa_fact-gen_ci\nEnter passphrase (empty for no passphrase): \nEnter same passphrase again: \nYour identification has been saved in id_rsa_fact-gen_ci\nYour public key has been saved in id_rsa_fact-gen_ci.pub\nThe key fingerprint is:\nSHA256:lUtVhsM0qmpQxmy3TOWuZx1ZbEy81pdoMXU3CfTGZm0 kumarharsh@Kumars-Macbook-Air.local\nThe key's randomart image is:\n+---[RSA 3072]----+\n|          .o=**o+|\n...\n|                 |\n+----[SHA256]-----+<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You will now be able to view the public key by running the following command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 cat id_rsa_fact-gen_ci.pub\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC01PFioPM8+7Paexj7euO7NCzY5wUuJJ+eLy1bmu0qT2n5mPx02mtOUT\/0jESSV4zPsaMov4Qz3kX0pHk7vCgzGdQ1uupPQe1qKPJglRTut0ZBzWauxmOff+6ysPi11lSrdLgY5zSKTgib746q7lDLKqR0DD2vNod9m6N6wO1xD9NBwbr\/GeYxhzp8Ct05VCMjLs2ejXN6h5aFmo1g1vcaiqFnTlK+hf6y2dBCIaUB3uoZNsysmMTqw+BJegpoI44zn7JKIquqg\/7BnIP6INMgyCjoNLC3kbHxoWtSbVskbE61HDvyQS1F2PVhwmJVpcNdUfSpf9kskT7UdaYhTroYIF+G0XmDEr4zKW9Sb0zlzaDdGFBu1yowH5BGBEyfSDGdx3W6HdDCygemuLUj6Huv0akv\/y6DhoH0FvJvQMLwJMvVd6kjhYDJ7VF0rcGp\/r2BcSBdbtNiTtXFIHPVFqLACilHJmfdLL78E9zwvci57h3trwZJbZ3fdSy1QeJ2kaU= kumarharsh@Kumars-Macbook-Air.local<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><em>For Windows, you can use the PuTTYgen key generator that comes with the PuTTY SSH client to generate the keys. You can find instructions for that <\/em><a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/use-ssh-keys-authentication\/#putty\"><em>here<\/em><\/a><em>.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You need to copy the value of the public key (starting from <code>ssh-rsa AA..<\/code> to <code>..Air.local<\/code> in the example above) and paste it into the popup box that opens on the <strong>Deploy a new server<\/strong> page along with a name to remember it by:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-66-1024x588.png\" alt=\"-\" class=\"wp-image-46217\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once done, click on the <strong>Save the SSH key<\/strong> button and the key will be added to the new server:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-67-1024x588.png\" alt=\"-\" class=\"wp-image-46218\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The key also gets automatically saved to your UpCloud account so that you can easily attach it to any resources that you create in the future.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Initialization Script<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Next, you have the option to add an <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/initialization-scripts\/\">initialization script<\/a> to the deployment.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-68-1024x588.png\" alt=\"-\" class=\"wp-image-46219\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Normally, right after you set up a cloud server, you will have a list of things to do before you start deploying your applications on it. It can include anything from upgrading installed packages to configuring tools and setting up user accounts. Initialization scripts help you automate these tasks instead of having to manually do them after the server is deployed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since you will be deploying a Node.js application to this server, you need to ensure that Node is installed on it. You will also need the <code>git<\/code> CLI tool to interact with the repository and pull in the code. And, since the project uses <code>yarn<\/code> to manage dependencies and scripts, you will need to install it as well.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To have the server automatically do that after it is deployed, add the following code snippet to the initialization script textbox:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">#!\/bin\/bash \nsudo dnf update -y\nsudo dnf install curl dnf-plugins-core -y\nsudo dnf module install nodejs:22 -y\nsudo dnf install git -y\nnpm install --global yarn\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Remember to add the line<code> #!\/bin\/bash<\/code> to the top of the script to make sure the OS understands what kind of interpreter it needs to run this script.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s how it will look when you\u2019ve set up the initialization script correctly:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-69-1024x588.png\" alt=\"-\" class=\"wp-image-46220\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">UpCloud also allows you to save initialization scripts and reuse them when creating servers. You can do that by clicking the <strong>+ Add as saved script<\/strong> button. Saved scripts will be available in the dropdown next to <strong>Load a saved script<\/strong> when you create a server next time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Naming the Server and the Host<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, you need to provide a name for the host and server. Choose anything simple and relevant, such as <code>random-facts-generator<\/code> for the host and <code>random-facts-generator-server<\/code> for the server:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-70-1024x588.png\" alt=\"-\" class=\"wp-image-46221\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Now, you are ready to deploy the server. Click on the <strong>Deploy<\/strong> button and wait for the server to be deployed:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-71-1024x588.png\" alt=\"-\" class=\"wp-image-46222\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You can click on the server name to view its details and state. Once it\u2019s deployed and started, you will see the state <strong>Started<\/strong> on its details page:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-72-1024x588.png\" alt=\"-\" class=\"wp-image-46223\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You are now ready to connect to the server!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can scroll down to the bottom of the server details page to find the instructions to connect to your newly created server via SSH:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-73-1024x588.png\" alt=\"-\" class=\"wp-image-46224\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Running the command <code>ssh root@&lt;server-ip-address&gt; <\/code>will connect you to the server via SSH. If you chose a custom location for your SSH key when creating it, you will need to either add it to your keystore by running <code>ssh-add .\/id_rsa_fact-gen_ci <\/code>before you run the ssh connect command or supply the private key file to the ssh command:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ssh -i .\/id_rsa_fact-gen_ci root@&lt;server-ip-address&gt;<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once you\u2019re connected successfully, you will receive a similar output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 ssh root@95.111.197.195\nThe authenticity of host '95.111.197.195 (95.111.197.195)' can't be established.\nED25519 key fingerprint is SHA256:0UMHPBBghugwpkt+MzrRRfQSRqUPKv3uEa4TkTaWTxY.\nThis key is not known by any other names.\nAre you sure you want to continue connecting (yes\/no\/[fingerprint])? yes\nWarning: Permanently added '95.111.197.195' (ED25519) to the list of known hosts.\n[root@random-facts-generator ~]# <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can now run commands on the server to set up your application for the first time. You can ensure that Node, git, and yarn are set up correctly by running the following commands:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">[root@random-facts-generator ~]# node -v\nv22.11.0\n[root@random-facts-generator ~]# git -v\ngit version 2.43.5\n[root@random-facts-generator ~]# yarn -v\n1.22.22<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Option 2: Through the <\/strong><strong>upctl<\/strong><strong> CLI<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">While the dashboard helps you carefully configure your server, it can be a time-consuming process. UpCloud also allows users to manage its resources through its CLI called <a href=\"https:\/\/github.com\/UpCloudLtd\/upcloud-cli\" target=\"_blank\" rel=\"noopener\">upctl<\/a>. You can find detailed instructions on how to set it up <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/get-started-upcloud-command-line-interface\/\">here<\/a> based on your operating system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have set up upctl correctly and authenticated with your UpCloud account, running the following command should print the details of your UpCloud account:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 upctl account show\n  \n  Username: kumarharsh \n  Credits:  \u20ac100.52   \n  \n  Resource Limits:\n    Cores:                      100 \n    Detached Floating IPs:       10 \n    Load balancers:              50 \n    Managed object storages:     20 \n    Memory:                  307200 \n    Network peerings:           100 \n    Networks:                   100 \n    NTP excess GiB:               0 \n    Public IPv4:                 20 \n    Public IPv6:                100 \n    Storage HDD:              10240 \n    Storage MaxIOPS:          10240 \n    Storage SSD:              10240 <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>upctl <\/code>can help you set up a server exactly the same as you saw above with just one command:<br><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl server create \n  --title \"Random Facts Generator Server\" \n  --zone sg-sin1 \n  --os \"AlmaLinux 9\" \n  --hostname random-facts-generator-server \n  --ssh-keys key\/id_rsa_fact-gen_ci.pub \n  --plan DEV-1xCPU-1GB-10GB \n  --user-data \"$(cat init-script)\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s a quick explanation of the arguments used:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2013 <code>title:<\/code> sets the name of the server<\/li>\n\n\n\n<li>\u2013 <code>zone: <\/code>allows you to choose the location of the server. You can run <code>upctl zone<\/code> list to get a list of available locations to pick from.<\/li>\n\n\n\n<li>\u2013 <code>os:<\/code> allows you to choose the operating system for your server. Not including this would set up the server with Ubuntu Server 24.04<\/li>\n\n\n\n<li>\u2013 <code>hostname:<\/code> allows you to set up the hostname<\/li>\n\n\n\n<li>\u2013 <code>ssh-keys:<\/code> allows you to provide the public SSH key for the server as a file<\/li>\n\n\n\n<li>\u2013 <code>plan:<\/code> allows you to choose the server plan. You can run the command <code>upctl server plans <\/code>to list available server plans and their details.<\/li>\n\n\n\n<li>\u2013 <code>user-data: <\/code>allows you to provide an initialization script to the server. This takes in a string value, so the command above has been configured to access the script stored in the file <code>init-script<\/code> through the <code>cat<\/code> command.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Once you run the command, you will receive a similar output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2713 Creating server random-facts-generator-server                                                                                   11 s\n  \n  UUID         0065e621-7114-4a22-8dc4-a6fee2763c65     \n  IP Addresses 10.10.1.98,                              \n               2a04:3543:1000:2310:78a8:8fff:feba:052c, \n               95.111.196.213                <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can run <code>upctl server list<\/code> to view the list of active servers along with their state:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 upctl server list\n\n UUID                                   Hostname                 Plan                 Zone      State   \n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n 00264c7f-51bd-4e5b-8aa7-603085afc65e   random-facts-generator   DEV-1xCPU-1GB-10GB   sg-sin1   started<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once the state is <strong>started<\/strong>, you can ssh into the server using the command <code>ssh root@&lt;server-ip-address&gt;.<\/code> You can use upctl to retrieve the server IP address by running the command<code> upctl server show &lt;server-name&gt;:<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u2717 upctl server show random-facts-generator\n  \n  Common\n    UUID:          00264c7f-51bd-4e5b-8aa7-603085afc65e \n    Hostname:      random-facts-generator               \n    Title:         random-facts-generator-server        \n    Plan:          DEV-1xCPU-1GB-10GB                   \n    Zone:          sg-sin1                              \n    State:         started                              \n    Simple Backup: no                                   \n    Licence:       0                                    \n    Metadata:      True                                 \n    Timezone:      UTC                                  \n    Host ID:       6108758659                           \n    Server Group:                                       \n    Tags:                                               \n\n  Labels:\n\n    No labels defined for this resource.\n    \n  Storage: (Flags: B = bootdisk, P = part of plan)\n\n     UUID                                   Title                                    Type   Address    Size (GiB)   Encrypted   Flags \n    \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n     01f55104-bb47-423b-b3d6-cc087fa99195   random-facts-generator-server Device 1   disk   virtio:0           10   no          P     \n    \n  NICs: (Flags: S = source IP filtering, B = bootable)\n\n     #   Type      IP Address                                      MAC Address         Network                                Flags \n    \u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n     1   public    IPv4: 95.111.197.195                            7a:a8:8f:ba:77:1d   03289129-7498-4133-84cd-d43b26adac8b   S     \n     2   utility   IPv4: 10.10.1.98                                7a:a8:8f:ba:a5:35   0372fde3-e376-4e4c-a646-22789035cec0   S     \n     3   public    IPv6: 2a04:3543:1000:2310:78a8:8fff:feba:0f0d   7a:a8:8f:ba:0f:0d   03000000-0000-4000-8030-000000000000   S  <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">At this point, you are ready to deploy the Node.js application on your newly created server and move forward with setting up the pipeline.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Deploying the Application for the First Time<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before you start deploying the Node.js application, make sure you have forked it to your GitLab account.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once ready, SSH into the server and run the following commands:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\"># Clone your forked repository\ngit clone https:\/\/gitlab.com\/&lt;your-gitlab-username&gt;\/random-facts-generator.git\n\n# change into the cloned directory\ncd random-facts-generator\n\n# Install dependencies\nyarn\n\n# Start the production server\nyarn start:prod<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This will start the Node.js application in a background process on the server. The Express app is configured to run on port 5001, so you should now be able to see the deployed app in action on the web address <code>http:\/\/&lt;your-server-ip&gt;:5001:<\/code><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-74-1024x616.png\" alt=\"-\" class=\"wp-image-46225\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This indicates that your server and app have been deployed successfully! Now, it\u2019s time to set up an automated pipeline that redeploys your app whenever you push a new commit to the <code>main <\/code>branch.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Creating a GitLab CI\/CD Pipeline<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To create the pipeline, go to the forked repository page and click on <strong>Build<\/strong> &gt; <strong>Pipelines<\/strong> from the left navigation pane:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-75-1024x587.png\" alt=\"-\" class=\"wp-image-46226\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will be taken to the Pipelines page of GitLab CI\/CD. Since the repo doesn\u2019t contain any pipelines right now, the page will provide you with options to set up your first pipeline:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-76-1024x587.png\" alt=\"-\" class=\"wp-image-46227\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If you scroll down below, you will find the Node.js template to start with. However, the template you choose for this pipeline does not matter since you are going to overwrite it anyway. Click on the <strong>Try test template<\/strong> button on the <strong>\u201cHello World\u201d with GitLab CI<\/strong> card:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-77-1024x587.png\" alt=\"-\" class=\"wp-image-46228\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will now be taken to the pipeline editor with a sample GitLab CI pipeline populated in the <strong>Edit<\/strong> tab:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-78-1024x587.png\" alt=\"-\" class=\"wp-image-46229\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This is where you will develop and deploy the first version of the pipeline. Once you deploy the pipeline, you can either choose the Pipeline editor from the left navigation pane on GitLab to edit it or just edit the <strong>.gitlab-ci.yaml<\/strong> file directly in your code editor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For this pipeline, you will use the latest Node image from Docker as the runtime. The workflow will look like this:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2013 Configure the pipeline runner with your UpCloud server\u2019s private SSH key<\/li>\n\n\n\n<li>\u2013 Connect to your UpCloud server through SSH from within the pipeline<\/li>\n\n\n\n<li>\u2013 Run a <code>git pull<\/code> on the local repo on the server to get the latest code<\/li>\n\n\n\n<li>\u2013 Restart the running PM2 process of the application using the <code>yarn restart:prod<\/code> command<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Some people might prefer using tools like <a href=\"https:\/\/www.freecodecamp.org\/news\/scp-linux-command-example-how-to-ssh-file-transfer-from-remote-to-local\/\" target=\"_blank\" rel=\"noopener\">scp<\/a> to directly copy files into the server instead of relying on fetching from a git repository independently, but since this tutorial uses a publicly accessible git repo, either method works fine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also, this pipeline will only contain the deploy stage. This is because Node.js apps do not need to be built most of the time (unless you are using a dialect like TypeScript or CoffeeScript to write the code), and this repo does not contain any tests. Still, if you are looking to add those steps to your pipeline, <a href=\"https:\/\/gitlab.com\/gitlab-org\/gitlab\/-\/blob\/master\/lib\/gitlab\/ci\/templates\/Nodejs.gitlab-ci.yml\" target=\"_blank\" rel=\"noopener\">GitLab\u2019s Node.js template<\/a> has got you covered.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now that you understand what the pipeline will do, let\u2019s start developing it!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Developing the Pipeline<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To start, replace everything in the editor with the following code snippet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\"># Official framework image. You can look for the different tagged releases at:\n# https:\/\/hub.docker.com\/r\/library\/node\/tags\/\nimage: node:latest\n\n# This folder is cached between builds\n# https:\/\/docs.gitlab.com\/ee\/ci\/yaml\/index.html#cache\ncache:\n  paths:\n    - node_modules\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This defines that the pipeline should use the Node.js image for the runtime and cache the <strong>node_modules<\/strong> folder in between pipeline runs to reduce pipeline run time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, add the following YAML node to the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">before_script:\n  ## Install ssh-agent if not already installed, it is required by Docker.\n  - 'command -v ssh-agent &gt;\/dev\/null || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )'\n\n  ## Run ssh-agent (inside the build environment)\n  - eval $(ssh-agent -s)\n\n  ## Decode the base64 encoded private key from environment variables and save it in a key file\n  - echo $UPCLOUD_SSH_PRIVATE_KEY | base64 -d &gt; id_rsa_fact-gen_ci\n  \n  ## Give the right permissions, otherwise ssh-add will refuse to add files\n  - chmod 400 id_rsa_fact-gen_ci\n  \n  ## Add the key to the SSH agent store\n  - ssh-add id_rsa_fact-gen_ci\n\n  ## Create the SSH directory and give it the right permissions\n  - mkdir -p ~\/.ssh\n  - chmod 700 ~\/.ssh\n\n  ## Run ssh-keyscan to get the public host key of your UpCloud server and add it to your CI runner's known hosts list\n  - ssh-keyscan -t rsa $SERVER_IP &gt;&gt; ~\/.ssh\/known_hosts<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>before_script <\/code>node is used to define setup\/initialization commands before running the pipeline. In the code above, you use this node to install <code>ssh-agent<\/code>, retrieve the private SSH key of your UpCloud server from a GitLab CI environment variable, decode it from base64, save it in a file, add the file to the SSH agent, and retrieve the host key of your UpCloud server using <code>ssh-keyscan<\/code> to store it in your runner\u2019s list of known SSH hosts. This will ensure that when you run <code>ssh root@&lt;your-server-ip&gt;<\/code> in the deploy job, it can connect to your UpCloud server without any hassle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As you can see, this code snippet uses two GitLab CI environment variables:<br><code>$UPCLOUD_SSH_PRIVATE_KEY<\/code> and <code>$SERVER_IP<\/code>. You will need to create those in your GitLab repository before you can try running this pipeline. Don\u2019t worry about it for now; you will see how to do that in a while.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An important thing to note here though is that the private SSH key isn\u2019t provided directly in the GitLab CI environment variable. This is because GitLab CI environment variables can be accessed by the debug logs or by people who have write access to the pipeline, which means storing the key directly here can expose it to people who do not need to access it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For sensitive credentials like these, GitLab recommends <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/variables\/#mask-a-cicd-variable\" target=\"_blank\" rel=\"noopener\">masking the environment variables<\/a>. While it is still not a guaranteed way to secure your credentials (someone running an <code>env<\/code> or <code>printenv<\/code> command can still dump them into the pipeline logs), it sure makes it a little harder to access the credentials directly. A key issue here though is that masked variables can not contain certain characters, such as <code>@<\/code>, <code>_<\/code>, <code>-<\/code>, or <code>:<\/code>. The raw key data might contain those, so you need to encode it into a base64 string before providing it to GitLab. This is the reason why this code snippet decodes the private key from base64 before saving it into the key file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now, the only part left is to add the deploy job itself to the workflow. You can do that by adding the following code snippet to the <strong>.gitlab-ci.yaml<\/strong> file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">deploy:\n  stage: deploy\n  script: \n    - ssh root@$SERVER_IP \/bin\/bash &lt;&lt; 'EOT'\n    - cd random-facts-generator\n    - git pull origin main\n    - yarn\n    - yarn restart:prod\n    - EOT\n  environment: production<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This defines a job named <code>deploy<\/code> that runs in the <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/yaml\/#stages\" target=\"_blank\" rel=\"noopener\">deploy stage<\/a> of the workflow. It runs the <code>ssh<\/code> command to connect to the UpCloud server and passes in a script to run on it. The script essentially does a git pull on the main branch and restarts the server. It also sets the target <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/yaml\/#stages\" target=\"_blank\" rel=\"noopener\">GitLab environment<\/a> to be <code>production<\/code>, but that is up to you to configure based on your project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This completes the development of the pipeline. Here\u2019s what the complete pipeline looks like at a glance:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">image: node:latest\n\ncache:\n  paths:\n    - node_modules\/\n\nbefore_script:\n  - 'command -v ssh-agent &gt;\/dev\/null || ( apt-get update -y &amp;&amp; apt-get install openssh-client -y )'\n  - eval $(ssh-agent -s)\n  - echo $UPCLOUD_SSH_PRIVATE_KEY | base64 -d &gt; id_rsa_fact-gen_ci\n  - chmod 400 id_rsa_fact-gen_ci\n  - ssh-add id_rsa_fact-gen_ci\n  - mkdir -p ~\/.ssh\n  - chmod 700 ~\/.ssh\n  - ssh-keyscan -t rsa $SERVER_IP &gt;&gt; ~\/.ssh\/known_hosts\n\ndeploy:\n  stage: deploy\n  script: \n    - ssh root@$SERVER_IP \/bin\/bash &lt;&lt; 'EOT'\n    - cd random-facts-generator\n    - git pull origin main\n    - yarn\n    - yarn restart:prod\n    - EOT\n  environment: production<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have completed the YAML file, click on the <strong>Commit changes<\/strong> button at the bottom of the page:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-79-1024x587.png\" alt=\"-\" class=\"wp-image-46230\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will notice that the pipeline starts running immediately and fails:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-80-1024x587.png\" alt=\"-\" class=\"wp-image-46231\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This is because you need to configure two GitLab CI environment variables to provide the base64 encoded SSH private key and the server public IP address to the pipeline. Let\u2019s do that next.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Configuring the Environment Variables<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To configure the GitLab CI environment variables, click on <strong>Settings<\/strong> &gt; <strong>CI\/CD<\/strong> from the left navigation pane:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-81-1024x587.png\" alt=\"-\" class=\"wp-image-46232\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will be taken to the CI\/CD settings page for your repo. On this page, expand the <strong>Variables<\/strong> section and click on the <strong>Add variable<\/strong> button to add environment variables for your pipeline:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-82-1024x587.png\" alt=\"-\" class=\"wp-image-46233\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In the sidebar that opens on the right, you can define the key and the value for your environment variables. For the server IP variable, leave everything as is and the <strong>Key<\/strong> as \u201cSERVER_IP\u201d and <strong>Value<\/strong> as the IPv4 address of your UpCloud server. You can get it from your UpCloud dashboard or through the upctl CLI:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-83-1024x587.png\" alt=\"-\" class=\"wp-image-46234\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once done, click on the <strong>Add variable<\/strong> at the end of the form to add the variable. Repeat the process to add the SSH private key as well. This time, you will need to change the <strong>Visibility<\/strong> of the variable to <strong>Masked and hidden<\/strong> and convert the private key into a base64 encoded string before entering it in the <strong>Value<\/strong> field using any base64 encoder tool. You can also run<code> base64 -i &lt;your-private-key-file-location&gt;<\/code> if you have the base64 CLI installed locally.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s what the variable will look like before you submit it:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-84-1024x587.png\" alt=\"-\" class=\"wp-image-46235\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once you click the <strong>Add variable<\/strong> button, you can now go to the pipeline page and run it again. This time it should run successfully!<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-85-1024x587.png\" alt=\"-\" class=\"wp-image-46236\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Running and Testing the Pipeline<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The pipeline is now set up and listening for new commits pushed to your repo. To test it out, you can try updating something in the repo, such as as the footer text in the <strong>public\/index.html<\/strong> file that says \u201cMade in 2024\u201d to change it to \u201cMade in 2025\u201d:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-86-1024x587.png\" alt=\"-\" class=\"wp-image-46237\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You will see that GitLab automatically runs the pipeline:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-87-1024x587.png\" alt=\"-\" class=\"wp-image-46238\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">And in a few minutes, the text on your public website URL is updated:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-88-1024x613.png\" alt=\"-\" class=\"wp-image-46239\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This means that your pipeline has been set up and connected to your UpCloud infrastructure successfully. This marks the end of the tutorial. You can find the code used in the tutorial in <a href=\"https:\/\/gitlab.com\/kharsh39\/random-facts-generator\/-\/tree\/final?ref_type=heads\" target=\"_blank\" rel=\"noopener\">this branch of the GitLab repository<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Troubleshooting Some Common Issues<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Creating and configuring SSH keys can be a tricky task. Here are a few common issues you might run into along with how to fix them:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Host key verification failed:<\/code> This means that your GitLab runner does not have the host key of the UpCloud server added to its <code>known_hosts <\/code>file. Make sure you\u2019ve added <code>ssh-keyscan -t rsa $SERVER_IP &gt;&gt; ~\/.ssh\/known_hosts<\/code> in your before_script.<\/li>\n\n\n\n<li><\/li>\n\n\n\n<li><code>Error loading key \"&lt;key-file-name\": error in libcrypto:<\/code> This indicates that something is wrong with your key file. Make sure it has the key, and that the key is complete and correctly formatted.<\/li>\n\n\n\n<li><\/li>\n\n\n\n<li><code>Pseudo-terminal will not be allocated because stdin is not a terminal:<\/code> This error can occur if you try configuring the pipeline to SSH into the server and then run the server restart commands one-by-one through stdin (instead of providing the script as a remote command to the SSH call). Since GitLab runners are remote machines without a direct stdin stream from a user, they do not support pseudo-tty allocation. To solve this, follow the script example above that passes the remote script as a remote command to the SSH call.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">When deploying a cloud initialization script for a newly created server, errors in command execution are possible. Whether it is missing a <code>-y<\/code> flag to say yes to downloading packages or it is a missing\/incorrect version tag for a dependency, you need to have access to the script\u2019s execution logs to be able to debug any issues you might run into. You can access that by SSH-ing into the server and running the command: <code>sudo grep cloud-init \/var\/log\/messages<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Conclusion<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">As you saw above, GitLab CI\/CD pipelines can help ensure that code changes in your Node.js application are efficiently built, tested, and deployed to the UpCloud server automatically. This automation not only enhances the scalability of your development efforts (meaning you can quickly deploy to servers for releases and merge request previews) but also provides the flexibility needed to adapt to changing project requirements easily.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So why not dive in and experiment with some advanced setups like <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/environments\/incremental_rollouts.html#blue-green-deployment\" target=\"_blank\" rel=\"noopener\">blue-green deployments<\/a>. You might just find new ways to make your deployment process even better!<\/p>\n","protected":false},"author":82,"featured_media":46241,"comment_status":"open","ping_status":"closed","template":"","community-category":[223],"class_list":["post-1939","tutorial","type-tutorial","status-publish","has-post-thumbnail","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/1939","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/tutorial"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/82"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=1939"}],"version-history":[{"count":0,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/1939\/revisions"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=1939"}],"wp:term":[{"taxonomy":"community-category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/community-category?post=1939"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}