GitLab CI/CD and UpCloud 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’s perfect for hosting your applications.
In this tutorial, you’ll learn how to set up your deployment pipeline from GitLab to UpCloud, creating a smoother, automated deployment workflow and achieving faster releases.
Why Deploy to UpCloud From GitLab CI/CD?
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.
Imagine a SaaS company rolling out microservices on UpCloud or a startup managing continuous delivery for its Node.js applications—GitLab CI/CD can help such teams forget about DevOps entirely and just focus on their app’s 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.
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.
Overview and Prerequisites
In this tutorial, you will first fork this GitLab repository 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.
To follow along, you will need the following:
- – A GitLab account. You will use this account to fork your copy of this GitLab repository on which you will set up the CI/CD workflow. You will learn more about this repository shortly.
- – An UpCloud account. You can sign up for a free trial here.
- – Basic understanding of SSH and GitLab CI/CD YAML configuration.
Once you have these in place, let’s 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:
// contents of index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5001;
// Array of random facts
const facts = [
"Honey never spoils.",
"A group of flamingos is called a 'flamboyance.'",
"Bananas are berries, but strawberries aren't.",
"Octopuses have three hearts.",
"Australia is believed to have lost to emus in the Great Emu War of 1932",
"A jiffy is an actual unit of time: 1/100th of a second.",
"Cows have best friends and get stressed when they are separated.",
"The shortest war in history lasted 38 minutes.",
"The Eiffel Tower can be 15 cm taller during the summer.",
"A small child could swim through the veins of a blue whale."
];
// Serve static files (HTML, CSS, JS)
app.use(express.static('public'));
// Endpoint to get a random fact
app.get('/random-fact', (req, res) => {
const randomIndex = Math.floor(Math.random() * facts.length);
res.json({ fact: facts[randomIndex] });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
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’s the source code for this page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Random Facts Generator</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 98vh;
background-color: #f5f5f5;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
p {
margin-top: 20px;
font-size: 20px;
text-align: center;
}
#footer {
position: absolute;
bottom: 10px;
font-size: 0.8rem;
}
</style>
</head>
<body>
<h1>Random Fact Generator</h1>
<button id="generate">Get a Random Fact</button>
<p id="fact"></p>
<p id="footer">Made in 2024</p>
<script>
document.getElementById('generate').onclick = async () => {
const response = await fetch('/random-fact');
const data = await response.json();
document.getElementById('fact').innerText = data.fact;
};
</script>
</body>
</html>
You can try running the app locally using the following commands:
# Clone the repo
git clone https://gitlab.com/kharsh39/random-facts-generator
cd random-facts-generator
# Install dependencies
yarn
# Run the development server
yarn dev
You can now go to http://localhost:5001 to view the app in action:

Once you click the Get a Random Fact button, you will, indeed, get a random fact.

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.
Now, let’s move on to setting up the UpCloud server where you will deploy this application.
Setting up an UpCloud Server
You can use either the UpCloud web dashboard or the upctl
CLI to set up an UpCloud web server. This section will walk you through both of these options. For beginners, it’s 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.
Option 1: Through the Web Dashboard
Head over to the UpCloud web dashboard and sign in with your UpCloud account. Once you log in, this is how your dashboard will look like:

Click on the + Deploy new button on the top right of the page:

Select Server from the list that pops up:

You will be taken to the Deploy a new server where you will configure the specifications of your server:

Server Location
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’ve chosen SG-SIN1 as it is the closest to my location.

Server Plan
Next up, you need to choose a server plan:

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 here.
Since this is a test project, choose the first option in the Developer list (1 CPU core, 1 GB RAM, and 10 GB storage).
Storage
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:

You can choose to add more storage devices at this point. You can learn how to do that here. For this tutorial, a 10 GB storage device is sufficient, so leave this section as it is and move ahead.
Automated Backups
Next, UpCloud offers you the option to set up automated backups of your server. Starting from the General Purpose server plans, the Day plan, 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.

For this tutorial, leave the automated backups disabled.
Operating System
After the hardware configuration is done, it’s 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 custom images.

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 AlmaLinux 9 as it is a lightweight option compared to enterprise distributions of Linux.
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.
Network
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 SDN private networks and attach them to the server if needed.

You can leave this section as it is and move ahead.
Optionals
This section allows you to enable or disable the metadata service for your server and configure anti-affinity server groups.

The metadata service 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. Anti-affinity server groups allow servers to be arranged into groups that try to place their members on separate hosts to achieve high fault tolerance.
Since you’ve 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.
Login Method
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.
AlmaLinux does not support one-time passwords, so you need to create an SSH key pair and add its public key here by clicking on the + Add new button

On MacOS and Windows environments, you will need the ssh-keygen tool to create the key pair. Open up a terminal window and run the following command:
ssh-keygen -t rsa
The tool will ask you to choose a location where the file will be saved:
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/kumarharsh/.ssh/id_rsa):
If you do not want to overwrite your current ~/.ssh/id_rsa key, provide a new file location, such as ./id_rsa_fact-gen_ci
, and press Enter.
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’s what the output will look like:
✗ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/kumarharsh/.ssh/id_rsa): id_rsa_fact-gen_ci
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_fact-gen_ci
Your public key has been saved in id_rsa_fact-gen_ci.pub
The key fingerprint is:
SHA256:lUtVhsM0qmpQxmy3TOWuZx1ZbEy81pdoMXU3CfTGZm0 [email protected]
The key's randomart image is:
+---[RSA 3072]----+
| .o=**o+|
...
| |
+----[SHA256]-----+
You will now be able to view the public key by running the following command:
✗ cat id_rsa_fact-gen_ci.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC01PFioPM8+7Paexj7euO7NCzY5wUuJJ+eLy1bmu0qT2n5mPx02mtOUT/0jESSV4zPsaMov4Qz3kX0pHk7vCgzGdQ1uupPQe1qKPJglRTut0ZBzWauxmOff+6ysPi11lSrdLgY5zSKTgib746q7lDLKqR0DD2vNod9m6N6wO1xD9NBwbr/GeYxhzp8Ct05VCMjLs2ejXN6h5aFmo1g1vcaiqFnTlK+hf6y2dBCIaUB3uoZNsysmMTqw+BJegpoI44zn7JKIquqg/7BnIP6INMgyCjoNLC3kbHxoWtSbVskbE61HDvyQS1F2PVhwmJVpcNdUfSpf9kskT7UdaYhTroYIF+G0XmDEr4zKW9Sb0zlzaDdGFBu1yowH5BGBEyfSDGdx3W6HdDCygemuLUj6Huv0akv/y6DhoH0FvJvQMLwJMvVd6kjhYDJ7VF0rcGp/r2BcSBdbtNiTtXFIHPVFqLACilHJmfdLL78E9zwvci57h3trwZJbZ3fdSy1QeJ2kaU= [email protected]
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 here.
You need to copy the value of the public key (starting from ssh-rsa AA..
to ..Air.local
in the example above) and paste it into the popup box that opens on the Deploy a new server page along with a name to remember it by:

Once done, click on the Save the SSH key button and the key will be added to the new server:

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.
Initialization Script
Next, you have the option to add an initialization script to the deployment.

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.
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 git
CLI tool to interact with the repository and pull in the code. And, since the project uses yarn
to manage dependencies and scripts, you will need to install it as well.
To have the server automatically do that after it is deployed, add the following code snippet to the initialization script textbox:
#!/bin/bash
sudo dnf update -y
sudo dnf install curl dnf-plugins-core -y
sudo dnf module install nodejs:22 -y
sudo dnf install git -y
npm install --global yarn
Remember to add the line #!/bin/bash
to the top of the script to make sure the OS understands what kind of interpreter it needs to run this script.
Here’s how it will look when you’ve set up the initialization script correctly:

UpCloud also allows you to save initialization scripts and reuse them when creating servers. You can do that by clicking the + Add as saved script button. Saved scripts will be available in the dropdown next to Load a saved script when you create a server next time.
Naming the Server and the Host
Finally, you need to provide a name for the host and server. Choose anything simple and relevant, such as random-facts-generator
for the host and random-facts-generator-server
for the server:

Now, you are ready to deploy the server. Click on the Deploy button and wait for the server to be deployed:

You can click on the server name to view its details and state. Once it’s deployed and started, you will see the state Started on its details page:

You are now ready to connect to the server!
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:

Running the command ssh root@<server-ip-address>
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 ssh-add ./id_rsa_fact-gen_ci
before you run the ssh connect command or supply the private key file to the ssh command:
ssh -i ./id_rsa_fact-gen_ci root@<server-ip-address>
Once you’re connected successfully, you will receive a similar output:
✗ ssh [email protected]
The authenticity of host '95.111.197.195 (95.111.197.195)' can't be established.
ED25519 key fingerprint is SHA256:0UMHPBBghugwpkt+MzrRRfQSRqUPKv3uEa4TkTaWTxY.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '95.111.197.195' (ED25519) to the list of known hosts.
[root@random-facts-generator ~]#
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:
[root@random-facts-generator ~]# node -v
v22.11.0
[root@random-facts-generator ~]# git -v
git version 2.43.5
[root@random-facts-generator ~]# yarn -v
1.22.22
Option 2: Through the upctl CLI
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 upctl. You can find detailed instructions on how to set it up here based on your operating system.
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:
✗ upctl account show
Username: kumarharsh
Credits: €100.52
Resource Limits:
Cores: 100
Detached Floating IPs: 10
Load balancers: 50
Managed object storages: 20
Memory: 307200
Network peerings: 100
Networks: 100
NTP excess GiB: 0
Public IPv4: 20
Public IPv6: 100
Storage HDD: 10240
Storage MaxIOPS: 10240
Storage SSD: 10240
upctl
can help you set up a server exactly the same as you saw above with just one command:
upctl server create
--title "Random Facts Generator Server"
--zone sg-sin1
--os "AlmaLinux 9"
--hostname random-facts-generator-server
--ssh-keys key/id_rsa_fact-gen_ci.pub
--plan DEV-1xCPU-1GB-10GB
--user-data "$(cat init-script)"
Here’s a quick explanation of the arguments used:
- –
title:
sets the name of the server - –
zone:
allows you to choose the location of the server. You can runupctl zone
list to get a list of available locations to pick from. - –
os:
allows you to choose the operating system for your server. Not including this would set up the server with Ubuntu Server 24.04 - –
hostname:
allows you to set up the hostname - –
ssh-keys:
allows you to provide the public SSH key for the server as a file - –
plan:
allows you to choose the server plan. You can run the commandupctl server plans
to list available server plans and their details. - –
user-data:
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 fileinit-script
through thecat
command.
Once you run the command, you will receive a similar output:
✓ Creating server random-facts-generator-server 11 s
UUID 0065e621-7114-4a22-8dc4-a6fee2763c65
IP Addresses 10.10.1.98,
2a04:3543:1000:2310:78a8:8fff:feba:052c,
95.111.196.213
You can run upctl server list
to view the list of active servers along with their state:
✗ upctl server list
UUID Hostname Plan Zone State
────────────────────────────────────── ──────────────────────── ──────────────────── ───────── ─────────
00264c7f-51bd-4e5b-8aa7-603085afc65e random-facts-generator DEV-1xCPU-1GB-10GB sg-sin1 started
Once the state is started, you can ssh into the server using the command ssh root@<server-ip-address>.
You can use upctl to retrieve the server IP address by running the command upctl server show <server-name>:
✗ upctl server show random-facts-generator
Common
UUID: 00264c7f-51bd-4e5b-8aa7-603085afc65e
Hostname: random-facts-generator
Title: random-facts-generator-server
Plan: DEV-1xCPU-1GB-10GB
Zone: sg-sin1
State: started
Simple Backup: no
Licence: 0
Metadata: True
Timezone: UTC
Host ID: 6108758659
Server Group:
Tags:
Labels:
No labels defined for this resource.
Storage: (Flags: B = bootdisk, P = part of plan)
UUID Title Type Address Size (GiB) Encrypted Flags
────────────────────────────────────── ──────────────────────────────────────── ────── ────────── ──────────── ─────────── ───────
01f55104-bb47-423b-b3d6-cc087fa99195 random-facts-generator-server Device 1 disk virtio:0 10 no P
NICs: (Flags: S = source IP filtering, B = bootable)
# Type IP Address MAC Address Network Flags
─── ───────── ─────────────────────────────────────────────── ─────────────────── ────────────────────────────────────── ───────
1 public IPv4: 95.111.197.195 7a:a8:8f:ba:77:1d 03289129-7498-4133-84cd-d43b26adac8b S
2 utility IPv4: 10.10.1.98 7a:a8:8f:ba:a5:35 0372fde3-e376-4e4c-a646-22789035cec0 S
3 public IPv6: 2a04:3543:1000:2310:78a8:8fff:feba:0f0d 7a:a8:8f:ba:0f:0d 03000000-0000-4000-8030-000000000000 S
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.
Deploying the Application for the First Time
Before you start deploying the Node.js application, make sure you have forked it to your GitLab account.
Once ready, SSH into the server and run the following commands:
# Clone your forked repository
git clone https://gitlab.com/<your-gitlab-username>/random-facts-generator.git
# change into the cloned directory
cd random-facts-generator
# Install dependencies
yarn
# Start the production server
yarn start:prod
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 http://<your-server-ip>:5001:

This indicates that your server and app have been deployed successfully! Now, it’s time to set up an automated pipeline that redeploys your app whenever you push a new commit to the main
branch.
Creating a GitLab CI/CD Pipeline
To create the pipeline, go to the forked repository page and click on Build > Pipelines from the left navigation pane:

You will be taken to the Pipelines page of GitLab CI/CD. Since the repo doesn’t contain any pipelines right now, the page will provide you with options to set up your first pipeline:

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 Try test template button on the “Hello World” with GitLab CI card:

You will now be taken to the pipeline editor with a sample GitLab CI pipeline populated in the Edit tab:

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 .gitlab-ci.yaml file directly in your code editor.
For this pipeline, you will use the latest Node image from Docker as the runtime. The workflow will look like this:
- – Configure the pipeline runner with your UpCloud server’s private SSH key
- – Connect to your UpCloud server through SSH from within the pipeline
- – Run a
git pull
on the local repo on the server to get the latest code - – Restart the running PM2 process of the application using the
yarn restart:prod
command
Some people might prefer using tools like scp 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.
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, GitLab’s Node.js template has got you covered.
Now that you understand what the pipeline will do, let’s start developing it!
Developing the Pipeline
To start, replace everything in the editor with the following code snippet:
# Official framework image. You can look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:latest
# This folder is cached between builds
# https://docs.gitlab.com/ee/ci/yaml/index.html#cache
cache:
paths:
- node_modules/
This defines that the pipeline should use the Node.js image for the runtime and cache the node_modules folder in between pipeline runs to reduce pipeline run time.
Next, add the following YAML node to the file:
before_script:
## Install ssh-agent if not already installed, it is required by Docker.
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
## Run ssh-agent (inside the build environment)
- eval $(ssh-agent -s)
## Decode the base64 encoded private key from environment variables and save it in a key file
- echo $UPCLOUD_SSH_PRIVATE_KEY | base64 -d > id_rsa_fact-gen_ci
## Give the right permissions, otherwise ssh-add will refuse to add files
- chmod 400 id_rsa_fact-gen_ci
## Add the key to the SSH agent store
- ssh-add id_rsa_fact-gen_ci
## Create the SSH directory and give it the right permissions
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
## Run ssh-keyscan to get the public host key of your UpCloud server and add it to your CI runner's known hosts list
- ssh-keyscan -t rsa $SERVER_IP >> ~/.ssh/known_hosts
The before_script
node is used to define setup/initialization commands before running the pipeline. In the code above, you use this node to install ssh-agent
, 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 ssh-keyscan
to store it in your runner’s list of known SSH hosts. This will ensure that when you run ssh root@<your-server-ip>
in the deploy job, it can connect to your UpCloud server without any hassle.
As you can see, this code snippet uses two GitLab CI environment variables:$UPCLOUD_SSH_PRIVATE_KEY
and $SERVER_IP
. You will need to create those in your GitLab repository before you can try running this pipeline. Don’t worry about it for now; you will see how to do that in a while.
An important thing to note here though is that the private SSH key isn’t 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.
For sensitive credentials like these, GitLab recommends masking the environment variables. While it is still not a guaranteed way to secure your credentials (someone running an env
or printenv
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 @
, _
, -
, or :
. 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.
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 .gitlab-ci.yaml file:
deploy:
stage: deploy
script:
- ssh root@$SERVER_IP /bin/bash << 'EOT'
- cd random-facts-generator
- git pull origin main
- yarn
- yarn restart:prod
- EOT
environment: production
This defines a job named deploy
that runs in the deploy stage of the workflow. It runs the ssh
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 GitLab environment to be production
, but that is up to you to configure based on your project.
This completes the development of the pipeline. Here’s what the complete pipeline looks like at a glance:
image: node:latest
cache:
paths:
- node_modules/
before_script:
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
- echo $UPCLOUD_SSH_PRIVATE_KEY | base64 -d > id_rsa_fact-gen_ci
- chmod 400 id_rsa_fact-gen_ci
- ssh-add id_rsa_fact-gen_ci
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -t rsa $SERVER_IP >> ~/.ssh/known_hosts
deploy:
stage: deploy
script:
- ssh root@$SERVER_IP /bin/bash << 'EOT'
- cd random-facts-generator
- git pull origin main
- yarn
- yarn restart:prod
- EOT
environment: production
Once you have completed the YAML file, click on the Commit changes button at the bottom of the page:

You will notice that the pipeline starts running immediately and fails:

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’s do that next.
Configuring the Environment Variables
To configure the GitLab CI environment variables, click on Settings > CI/CD from the left navigation pane:

You will be taken to the CI/CD settings page for your repo. On this page, expand the Variables section and click on the Add variable button to add environment variables for your pipeline:

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 Key as “SERVER_IP” and Value as the IPv4 address of your UpCloud server. You can get it from your UpCloud dashboard or through the upctl CLI:

Once done, click on the Add variable 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 Visibility of the variable to Masked and hidden and convert the private key into a base64 encoded string before entering it in the Value field using any base64 encoder tool. You can also run base64 -i <your-private-key-file-location>
if you have the base64 CLI installed locally.
Here’s what the variable will look like before you submit it:

Once you click the Add variable button, you can now go to the pipeline page and run it again. This time it should run successfully!

Running and Testing the Pipeline
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 public/index.html file that says “Made in 2024” to change it to “Made in 2025”:

You will see that GitLab automatically runs the pipeline:

And in a few minutes, the text on your public website URL is updated:

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 this branch of the GitLab repository.
Troubleshooting Some Common Issues
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:
Host key verification failed:
This means that your GitLab runner does not have the host key of the UpCloud server added to itsknown_hosts
file. Make sure you’ve addedssh-keyscan -t rsa $SERVER_IP >> ~/.ssh/known_hosts
in your before_script.Error loading key "<key-file-name": error in libcrypto:
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.Pseudo-terminal will not be allocated because stdin is not a terminal:
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.
When deploying a cloud initialization script for a newly created server, errors in command execution are possible. Whether it is missing a -y
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’s 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: sudo grep cloud-init /var/log/messages
.
Conclusion
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.
So why not dive in and experiment with some advanced setups like blue-green deployments or Dockerized apps? You might just find new ways to make your deployment process even better!