An Indie Hacker's Guide to DevOps

Building a CI/CD pipeline for your VPS Machine

This article is sponsored by Subbit.co. Get Early Access

So here’s how this is going to go. In this article, you will get the full indie hacker’s guide to deploy a server from local to live. I will skip the nuances involved in each step and just walk you through the happy path. This means a lot of copying and pasting will be involved. After which I will link to articles that explain the nuances of each step and why each decision was made.

Before we begin, I’m Tenotea (Give me a follow)🙃. I just hit five years as a software engineer and I realized I have learnt a lot and letting all that information sit pretty in my head is not the right way to go. So here’s to the first of many. Let’s begin!

Prerequisites

  1. A Github Account: Because our CI/CD Pipeline will be set up using Github Actions.

  2. A Node.js Application: Because we need something to deploy (obviously).

  3. A VPS (Virtual Private Server) Machine: Because we need somewhere to deploy to. Unfortunately, I do not have access to something free but Hostinger(my affiliate link btw) has a lot of good deals on VPS machines. An EC2 instance would also work wonders here.

  4. An Open Mind: This is an indie hacker’s guide 💀.

Chapter 1: Prepping the VPS Machine

I have installed Ubuntu 22 on my VPS machine. The Node.js server would require the installation of the Node.js runtime. Hence, the first step is to log into the VPS Machine. There are many different ways to achieve this so confirm with your hosting provider. If you’re using Hostinger, the command below would work for you. Read a Complete SSH Guide

$ ssh root@<ip-address>

Once in, next step is to install Node Version Manager(NVM). Read the Complete Installation Guide

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
$ source ~/.bashrc
$ nvm install 22

After running these three commands, you should have Nodejs v22 installed, which is the current LTS. If you’re using yarn as your package manager, now is the right time to install that. But for your sanity, I strongly recommend you stick with NPM and use yarn only if you have to. This is also the part where you install other server dependencies you might require like Your database, Redis, and so on. We’re self hosting remember?

Now that we have our runtime requirement satisfied, we need to setup our version control requirement. What we’re gonna do here is grant the SSH key of this VPS Machine access to our github account. While there is a way to link per repository, I vaguely remember being blocked from using the same token for multiple repositories so this happens to be my de facto solution. (Like i said, keep an open mind).

First you want to generate an SSH key on the VPS Machine (VPS machine, not your local machine). Read the full guide here

$ ssh-keygen -t rsa -b 4096

Continue hitting enter till it stops prompting you. DO NOT SET A PASSWORD! A public key should be saved to a default location: ~/.ssh/id_rsa.pub. Run the command below to reveal its content.

$ cat ~/.ssh/id_rsa.pub

Copy whatever appears in the terminal and head over to your github account.

Navigate to Settings > SSH and GPG Keys > New SSH Key

Give the key a name and just paste in the content of the .pub file as-is into the text area and hit save.

Head over again to your VPS Machine (remember to login if you’ve been booted out) and now we want to verify that our SSH setup works. To do this, we perform an SSH Host Verification using the following command.

$ ssh -T git@github.com

Respond to the prompt with yes or y and your server is ready to pull your code from git.

As a final requirement for this guide, we will be installing PM2. This will manage the health of our server while in production. The beauty of PM2 is it’s not specific to Javascript applications, meaning you can manage any server written in any language. To install PM2

$ npm install pm2@latest -g

To prevent errors in the next chapter, we need to do some house cleaning by granting sudo access the npm and pm2 binaries. Run the following commands to set that up.

$ sudo ln -s "$NVM_DIR/versions/node/$(nvm version)/bin/node" "/usr/local/bin/node"
$ sudo ln -s "$NVM_DIR/versions/node/$(nvm version)/bin/npm" "/usr/local/bin/npm"
$ sudo ln -s "$NVM_DIR/versions/node/$(nvm version)/bin/npx" "/usr/local/bin/npx"
$ sudo ln -s "$NVM_DIR/versions/node/$(nvm version)/bin/pm2" "/usr/local/bin/pm2”
💡
I want to say this is Node.js specific but my last deployment with bun also had me doing the same thing but with the bun binaries. So if you’re getting “Command xxx not found“ error from the next chapter, come back here to fix it.

And that’s it for prepping up the VPS Machine.

Quick word from today’s sponsor, Subbit.co

Subbit - Smart billing solutions for modern businesses

Building your next product and looking to handle subscriptions? Don’t let that delay you from shipping! Subbit is the subscriptions manager your payment provider won’t give you. We’ll manage any kind of subscription on your behalf (Usage based, Recurring, Lifetime e.t.c) and can integrate with any payment provider (Stripe, Square, Flutterwave, Paystack e.t.c).

We’re currently receiving applications for our Private Beta. Visit our website subbit.co to be among the first to try it out!

Chapter 2: CI/CD Pipeline With Github Actions

This section describes the very simple procedure of Logging into the VPS machine from Github Actions and running a bash script that pulls our codebase from Github and starts the server/application.

Alright! Let’s break it down

Lines 1 - 5 is how we tell Github to run this action when a push is made to the development branch. This will also trigger when you merge a pull request into development. workflow_dispatch will help run this script manually from the Actions dashboard. Read about Github Actions here

Line 10 is stating that this action should run on ubuntu which matches the Operating System on our VPS Machine.

So let’s talk about Github Secrets because the next ones rely heavily on it. They are Github’s way of hiding sensitive information and Github Actions can read their values during execution. To create secrets for your Github action;

Navigate to Repo > Settings > Secrets and Variables > Actions > New Repository Secret

Here we are creating 3 variables:

  1. SSH_ADDRESS which is the IP Address of the server,

  2. SSH_USER which is the username used to log into the server.

  3. SSH_PRIVATE_KEY which is a private key that belongs to a computer that already has access to the VPS Machine. There is a huge chance that computer your local machine. Remember during the server setup, we used the public key of the VPS Machine to connect to GitHub, now, we’ll use the private key of your local machine provided it has access to the VPS Machine.

    Run the following command to get the private key

     $ cat ~/.ssh/id_rsa
    
    💡
    The content of the private key should start with -----BEGIN OPENSSH PRIVATE KEY----- and end with -----END OPENSSH PRIVATE KEY-----. Do not attempt to modify or omit from this content. Copy the content as is and paste into the text box.

Back to the breakdown!

Line 13 is importing an action from the Github Actions Marketplace. appleboy/ssh-action@master. This action is how we are able to log into our VPS Machine. But to do that, we need to provide it with the SSH_ADDRESS, SSH_USER and SSH_PRIVATE_KEY. Think of it this way, the action has to run ssh root@<ip-address> to actually enter the server. So, we need to give it all it needs which is what we’re doing on lines 17 to 19.

On Line 15, We’re importing values from Github secrets to use as the .env file of our Node.js application.

  1. DOT_ENV: ${{secrets.DOT_ENV}} in plain english is saying “Create the Environment Variable DOT_ENV on ssh-action (the imported action)”.

  2. As you can see*,* we are now referencing the created environment variable on line 20. This will make DOT_ENV available to the bash script on line 46 where we dump the content of the variable into a .env file.

This way, Github Secrets becomes a secrets manager, not just for our action, but also for our backend application.

Line 52 first checks if the server is already being managed by pm2. if yes, it reloads it else it spins it up. In this example, I am storing my start command in a pm2 ecosystem file. Feel free to replace the command with what would spin up your application. The contents of my ecosystem.config.js file is below

module.exports = {
  apps: [
    {
      name: "docs.subbit",
      script: "npm start",
    },
  ],
};

Other things to note

  1. git stash is being used to erase uncommitted changes the server may have made during the course of deployment.

  2. npm run build is an optional step if your server doesn’t need a build step (bun geng say hi in the comments 😌)

If all is set up correctly, go ahead and push your changes to your development branch and watch the magic happen. Visit your server at ip-address:port to confirm your server is up and running.

Chapter 3: DNS, Reverse Proxy & SSL with Caddy Server

Caddy server is an alternative to NGINX. It provides a far better developer experience as far as configurations go plus it automatically provides you with an SSL certificate. This step is very useful if you have a domain to tie your server to.

First we install caddy using the command below: Read the full installation guide here.

$ sudo apt install caddy

Next we modify the caddy configuration. Open it up using

nano /etc/caddy/Caddyfile

And then create your reverse proxy config using by pasting the following

<domain.com> {
    reverse_proxy localhost:<application-port>
}

Replace <domain.com> with your actual domain name, and the <application-port> with the port on which you’re serving your application e.g 3000 if on localhost:3000.

Once done, restart caddy server to apply this new configuration using

$ systemctl reload caddy

Now, to make your domain aware it should serve the content you want it to, you need update your DNS records. This is greatly subjective as there a plethora of DNS managers but Cloudflare I stan.

Create an A Record pointing <domain.com> to the IP_ADDRESS of your VPS machine.

Now type in your connected domain to your browser 🚀.

Chapter 4: Setting up Firewall using UFW

Now that caddy server is managing our http requests, we can now sunset access to the server from unwanted channels using a firewall. This will greatly reduce the chances of your server getting compromised. For this, we will be using UFW (Uncomplicated Firewall). Read the full setup guide here

UFW should come installed with your ubuntu installation. Run the following commands in order to quickly set it up.

$ sudo ufw allow OpenSSH
$ sudo ufw allow proto tcp from any to any port 80,443
$ sudo ufw enable

The first command ensures you still have ssh access to your server else you risk locking yourself out. The second command ensures only tcp connections to port 80 and 443 will be accepted by your server.


And that’s is it. Let me know if you enjoyed this read. I would greatly love to hear your opinion about it in the comments.

Until next time, Happy Grinding!