Setup / Web Hosting / Home Lab

How this Site is Built & Run.

May 28, 20237 min read
Building this Site
VS Code, connected via SSH to a Node.js container on Debian 12

This website is entirely self-hosted, with an automated deployment. Because I'm hosting it myself, I prefer a site thats lightweight, fast to display, and easy to deploy.

Lets start with how the site is built.

Node, Gatsby, and VS Code

To develop the site, first we need a Node LTS system. On Proxmox, I created a new container based on Debian 12, named gaffnode-lts. We then create a new user for SSH access:

adduser newusername
usernmod -aG sudo newusername

The new user now has sudo access, allowing me to remotely manage the container as well. This can be handy during development to add any new system packages that may be needed for development. To start, apt should be updated, and we will want a few tools (wget, curl, git) should be installed so that NVM - Node Version Manager - can be installed as well.

# Update apt repositories
sudo apt update
# Upgrade to current
sudo apt upgrade
# Add the tools
sudo apt install curl wget git

We are now ready to add Node.JS. Per their site, we can install the LTS version with:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

This is potentially dangerous. A shell script downloaded from a remote host being piped directly to bash, which many provide as their installation instructions, so this is not a Node.js specific comment. There are a few possible issues that can crop up:

  • Interrupted connections
  • Man-in-the-Middle (MITM) attacks
  • Pipe to bash detection

Webservers can detect if the output of curl or wget is being piped to bash, and the payload can be altered. Luke Spaderman provided an excellent writeup on that last method, which I think is worth checking out. Rather than run the script as shown by Node.js, my preference is to download the shell script, review the contents, and then run it:

wget https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh -O nvminstall.sh
nano ./nvminstall.sh

This is the alt tag.

This is the shell script, now downloaded and ready to be inspected and run.

# Installing NVM from the shell script
./nvminstall.sh | bash
# Verify the installation of NVM
nvm --version
# Downloads and installs the LTS version of Node.js
nvm install 20
# Verify the installation of Node.js
node -v
# Verify the installation of npm
npm -v
# Multiple versions of Node.js may be installed.  You can manually set the version to be used.
nvm use 20

Now that Node.js LTS is installed, what comes next is the framework to use. Gatsby is a React-based framework for static site generation, which is highly performant and scalable. Its easy to use Gatsby with Azure Static Web Apps, Github Pages, AWS Amplify, or even a simple nginx deployment as I'm using for this site.

# Install the Gatsby CLI
npm install -g gatsby-cli
# Verify the installation of the Gatsby CLI
gatsby --version

From this point forward, we can use VS Code for everything we need on that server. In VS Code, I use the Remote SSH extension to connect to the Node.js LTS host container, and create the site itself. Any template can be the start, but I like the Gatsby Starter Portfolio Minimal Theme, so I'm using that as the example here, as its also the theme used for this site.

gatsby new site_name https://github.com/konstantinmuenster/gatsby-starter-portfolio-minimal-theme
cd site_name
gatsby develop # Alternatively, npm run develop will work here as well.

If this is done within VS Code as part of a remote SSH session, the port will be forwarded, and the web page can be opened and edited. Its important to note that the site uses React (and GraphQL), but blog posts/articles can be written in markdown. I use Joplin for my notes, concept writeups, etc. already, which makes transferring content over for editing quite easy.

Deploying nginx and Jenkins

The next piece of the puzzle is nginx, which I'm using to host the statically generated site. Rather than another LXC, I decided to try out a self-hosted docker compose.yaml oriented management tool - Dockge. For other docker containers, I have an LXC running docker, but I wanted to try out something new. Dockge is clean, simple to use, and reminds me a bit of Uptime-Kuma in design. Pretty handy, but overall I think I prefer just writing my compose files in nano. That said, the compose file for nginx is simple:

version: "3.8"
services:
  nginx:
    image: nginx:latest
    restart: unless-stopped
    ports:
      - 8443:80
    volumes:
      - /mnt/mount_location:/usr/share/nginx/html

That mounted location is a dedicated share from another system, which provides me with multiple ways I can manually update the site if I so chose. Why choose manual updates though when we can do it automatically? This is precisely what Jenkins is for.

I already have Jenkins up and running as an LXC, and its pretty straightforward. Much like with the Node.js LTS LXC, in Proxmox we create a new container, create a new user, add that user to sudoers, update, upgrade. The instructions for Jenkins installation can be found here.

# Add the keyring and apt repository for Jenkins (Debian in this case)
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
  https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
  https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
  /etc/apt/sources.list.d/jenkins.list > /dev/null
# Update apt
sudo apt update
# Install Jenkins
sudo apt install jenkins

Quick and easy to get going. However, for the pipeline here, we want to copy from one container to another. To do so, I like the SSH Pipeline Steps plugin. This can be installed by going to Manage Jenkins > Plugins > Search for SSH Pipeline Steps. The first thing to do is to have the Node.js host build the project with gatsby build or npm run build. So we define a remote ssh command:

node {
  def remote = [:]
  remote.name = 'nodejs-lts'
  remote.host = 'nodejs-lts.gaffney-household.com' // Or the IP address, if you don't have local DNS set up for this
  remote.user = 'username'
  remote.password = 'password'
  remote.allowAnyHosts = true
  stage('Remote SSH') {
    sshCommand remote: remote, command: "cd site_name"
    sshCommand remote: remote, command: "gatsby build"
  }
}

We could also write a script to do this, and call the script instead, but I prefer it inside Jenkins instead. Now that the build is prepped, we need the contents of the generated /public subdirectory of site_name. For that, we can use an sshGet and sshPut. As both the nginx container being run as well as the nodejs container being run have no real access to the rest of my network, this is the right way to go. Similar jenkins file, but with a slight change:

    remote.allowAnyHosts = true
    stage('Remote SSH') {
      sshGet remote: remote, from: '~/site_name/public/**/*', into: '/public', override: true
    }

And one more with an sshPut instead. This wraps the automation itself, which is great! However, not quite automated enough...

In Jenkins, we can build periodically, build from a GitHub hook, or trigger builds remotely. As /public will only update when gatsby build / npm run build are called, the /public directory stays static. So I opted to have it run once a week on Sundays at 3am. This is because I usually make edits to my notes and writeups throughout the week, and then during naptime for my son, finalize writeups. If I decide to bring anything into this site, I can do so during that time.

I also chose to enable a triggered build. If I wanted to get some work done and see it right away, I could just use npm run develop or gatsby develop to see it locally, but I may want to see it running on my site live. Which I can now call with jenkins.gaffney-household.com/job/Gaffney-Household-Web/job/test/build?token=buildgatsbywebnow. Despite the full url here, that is provided from local DNS and not externally visible, making for a perfectly secure URL to trigger my build job and share it with the world.

If you have any questions, please contact me and let me know!

Node.jsGatsbynginxJenkins