Deploying SvelteKit with NodeJS to a Server Using Gitlab and PM2

2022/09/26

I recently ported this website from being statically generated with Elderjs to dynamically generated with SvelteKit and a NodeJS adapter. Here's a step-by-step guide on deploying your SvelteKit site to a remote server (in this case Ubuntu 22.04).

Setting up Demo App

If you don't already have a project, you can setup a quick demo using:

npm create svelte@latest my-app
cd my-app
npm install
npm run dev -- --open

There's more info at the SvelteKit site. Don't forget to install the node-adapter and update svelte.config.js as is outlined here

Setting up the Server

Get a VPS from whatever provider you want (I use Hetzner because they're cheap and reliable. That's my referral link). Install whatever server image you want (if you use Ubuntu 22.04, you'll be able to copy and paste all of the commands in this tutorial). Get your IP address and SSH into the server. You'll also want to setup an A type DNS record to point your domain to this new IP address (if you have a domain). No worries if not, you'll just be able to see the page by typing your IP address into the address bar.

ssh root@IP_ADDRESS

Creating Users

Create two users, one for deploying and one for using around the server (change USERNAME to whatever you want, and use that for the rest of the tutorial wherever you see USERNAME).

adduser deployer
adduser USERNAME

From a security perspective, you want to generate unique SSH keys for each user. Run the following twice on your LOCAL machine to generate two separate SSH keys.

ssh-keygen -b 521 -t ECDSA

The ECDSA is now preferred over the RSA algorithm for generating the keys.

Now on the server, add the keys to each user.

usermod -aG sudo deployer
sudo su deployer
mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod -R go= ~/.ssh
vim ~/.ssh/authorized_keys

Copy and paste one of the PUBLIC versions (.pub) of the keys you just generated into the file and save it. Type exit like below to sign out.

exit
usermod -aG sudo USERNAME
sudo su USERNAME
mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod -R go= ~/.ssh
vim ~/.ssh/authorized_keys

Copy and paste the second key's PUBLIC version (.pub) into the file and save it. You can now type exit to go back to root, then sign out and SSH back in using this user (ssh -i ~/.ssh/path/to/id_ecdsa USERNAME@IP_ADDRESS), or you can do the rest of the tutorial as root, in which case you won't have to add sudo to the front of every command.

Downloading Necessary Dependencies

Run updates

sudo apt update && sudo apt upgrade

Restart the system if you need to after upgrading

sudo systemctl reboot

SSH back in the same way as before.

Node Verion Management with n (nodejs + npm)

Install n for managing different version of NodeJS.

curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n
bash n lts
# Now node and npm are available
npm install -g n

You'll now have access to NodeJS and NPM.

NGINX

NGINX is going to be used for reverse proxying the incoming requests.

sudo apt install nginx

Postgres (optional)

If you're going to use Postgres, follow this to set it up and create a PG superuser for yourself.

sudo apt install postgresql postgresql-contrib
sudo su postgres
createuser --interactive

then make a DB for your user, replacing username with your chosen username.

createdb USERNAME 
exit

Download PM2

Install PM2 globally using

npm install pm2 -g

Setting up Gitlab

All you need to do to have Gitlab deploy the project for you is make a .gitlab-ci.yml file, have runners enabled, and generate a new ECDSA id, which you'll add to your project's CI/CD variables. First generate the key with the same command as before, on your LOCAL machine.

ssh-keygen -b 521 -t ECDSA

Copy the PRIVATE version of the key (NOT .pub), to a variable called SSH_PRIVATE_KEY in your Gitlab project's Settings -> CI/CD -> Variables -> Add Variable -> Key: SSH_PRIVATE_KEY, Value: paste in the key's contents.

Gitlab dashboard where you update the CI/CD settings.

Once you've done that, SSH into your server as the deployer user

ssh -i ~/.ssh/path/to/deployer/private/key deployer@IP_ADDRESS

And add the public version (.pub) of the SSH key you just generated for Gitlab to the ~/.ssh/authorized_keys file.

Now add a file called .gitlab-ci.yml to you project repo and copy in the contents below. You don't need to change anything in it, but make sure your Gitlab runners are running on Docker.

# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:18-alpine

stages:
  - deploy

deploy:
  stage: deploy
  before_script:
    # Install ssh-agent if not already installed, it is required by Docker.
    # (change apt-get to yum if you use a CentOS-based image)
    - 'which ssh-agent || ( apk add --update openssh )'

    # Add bash
    - apk add --update bash

    # Add git
    - apk add --update git

    # Run ssh-agent (inside the build environment)
    - eval $(ssh-agent -s)

    # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
    - echo "$SSH_PRIVATE_KEY" | ssh-add -

    # For Docker builds disable host key checking. Be aware that by adding that
    # you are suspectible to man-in-the-middle attacks.
    # WARNING: Use this only with the Docker executor, if you use it with shell
    # you will overwrite your user's SSH config.
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *
	StrictHostKeyChecking no

" > ~/.ssh/config'
    # In order to properly check the server's host key, assuming you created the
    # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines
    # instead.
    # - mkdir -p ~/.ssh
    # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
  script:
  - npm i -g pm2
  - pm2 deploy ecosystem.config.js production
  only:
  - main

Setting up PM2

For PM2 to work, you'll need to add an ecosystem.config.js file in your project repo and copy in the content below. If you're using TypeScript, you can use an ecosystem.config.cjs file since it uses module.exports instead of the ESM export default standard.

module.exports = {
	/**
	 * Application configuration section
	 * http://pm2.keymetrics.io/docs/usage/application-declaration/
	 */
	apps: [
		// First application
		{
			name: 'website.com', // name of the process in PM2
			script: 'build/index.js',
			env_production: {
				NODE_ENV: 'production',
				PORT: 5173 // port the app will be launched on
			}
		}
	],

	/**
	 * Deployment section
	 * http://pm2.keymetrics.io/docs/usage/deployment/
	 */
	deploy: {
		production: {
			user: 'deployer', // deployer user
			host: 'IP_ADDRESS', // IP address of your server
			ref: 'origin/main', // the branch you want to deploy
			repo: '[email protected]', // the ssh git clone URL
			path: '/var/www/website.com/', // the path where you want the project to be
			// code you want to run after the project has been pushed to your server
			'post-deploy':
				'npm install && npm run build && pm2 reload ecosystem.config.js --env production'
		}
	}
};

In apps change the name of app and PORT in env_production to whatever you want. This will be the port your node server will be deployed on. Just make sure it's free and make a note of it, because you'll need it for the NGINX config later.

In deploy, change the host to your server's IP address, ref to whatever branch you're using (main or master normally), edit the repo URL to match your repo URL on Gitlab (you can just copy the same URL you use when you clone the repo using SSH), and adjust the path to your website's name. You can also adjust the post-deploy scripts, but what is currently there should work for SvelteKit with the node adapter.

Now SSH into your server using the deployer user

ssh -i ~/.ssh/path/to/deployer/key deployer@IP_ADDRESS

Make a folder in your home directory called whatever you want the website to be called

mkdir ~/website.com

Then move it over to /var/www/ using sudo

sudo mv ~/website.com /var/www/

Now you have a folder you can deploy into as the deployer user. The destination path should be the same as what you have for path in your ecosystem.config.js file.

On your LOCAL machine, make sure you have pm2 installed (preferably with your package manager), or run the following to install it globally using npm

npm install pm2 -g

In the project repo on your LOCAL machine, now you can run:

pm2 deploy ecosystem.config.js production setup

It should setup the repo in /var/www/website.com on your remote server. Now you can also deploy from Gitlab by pushing a commit and watching it automatically deploy in the CI/CD jobs section.

NGINX Config

To setup NGINX on your server, you'll need to make a config file for the website, enable it, check it, and restart NGINX. SSH into your server, make the file, and change website.com to whatever domain you want.

sudo touch /etc/nginx/sites-available/website.com
sudo vim /etc/nginx/sites-available/website.com

Then copy the config below into the file. Be sure to change any intance of website.com to your domain and/or the port in proxy_redirect to whatever port you want your app to run on (same as in your ecosystem.config.js file).

server {

        access_log /var/log/nginx/website.access.log;
        error_log /var/log/nginx/website.error.log debug;


        server_name website.com www.website.com;

        location / {
                proxy_redirect http://localhost:5173/ https://$server_name/;
                proxy_pass http://localhost:5173;
                proxy_cache_bypass $http_upgrade;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';

               	include proxy_params; 
        }
 }

You can see at the top what files NGINX logs will go to. These are useful for debugging potential issues you might run into. Now link that config file to sites-enabled.

sudo ln -s /etc/nginx/sites-available/website.com /etc/nginx/sites-enabled/

Check the config and restart NGINX.

sudo nginx -t
sudo systemctl restart nginx

NGINX should now be serving your project or demo app on your domain and you can check by navigating to it in your browser.

HTTPS Setup (optional)

If you want to setup HTTPS support for your domain, you can use Let's Encrypt by running the commands below one by one to install the required dependencies, then register the domains.

sudo apt install snapd
sudo snap install snap-store


sudo snap install core 
sudo snap refresh core
sudo apt remove certbot

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

sudo certbot --nginx -d website.com -d www.website.com

sudo systemctl status snap.certbot.renew.service

sudo certbot renew --dry-run

Now you should have HTTPS setup too!


Want to see more in-depth content?

subscribe to my newsletter!

Other Articles