I have a task on hand that keeps me busy trying to find a good open source, self hosting commenting software that is lightweight and still looks good.

Finally, I want to settle with Schnack, which is really lightweight and runs node.js, which is really cool because the language is platform neutral, and I would have a chance to run it on ARM processors. So I started to visit its git repository to see what I can do.

Schnack's github repository provided a lot of useful information on how to setup a docker image. However, its Dockerfile is a bit outdated, which pulled a very outdated node:boron image.

Hence, I have to modify the Dockerfile and pull in a better node.js image. After searching the relevant repository for a while, I settled for the lts-buster image. So the relevant line in the Dockerfile looks like below:

# From node:boron to node:lts-buster
FROM node:lts-buster

Also, the original idea from Schnack is that if you want to run persistent storage, you simply can copy all the source codes into your own machine, and mount the whole volume through the following command:

docker run -p 3000:3000 -v $(pwd):/usr/src/app -d schn4ck/schnack

Which is very simple. However it may not be the most secure thing in the world.

Hence, I have decided do so some changes in the source codes to segregate the code, configuration and data, which all reside in /usr/src/app, into 3 different folders:

  • Codes - /usr/src/app
  • Data - /usr/src/app/data
  • Configurations - /usr/src/app/config

This allows the docker to be more flexible and use persistent volume to configure and store data. This is done by a few lines of changes in his original source codes.

Changing the config folder

Configurations are originally stored in schnack.json in the /usr/src/app working folder of the application, as per Schnack's website. (The website mentioned config.json, which is outdated).

A peek into the source codes config.js reveals the configuration file location as per below:

const nconf = require('nconf');
const crypto = require('crypto');

nconf
    .argv()
    .file({ file: './schnack.json' })
    .env()

So this is easy, I just changed it to:

nconf
    .argv()
    .file({ file: './config/schnack.json' })
    .env()

Done

Changing the data folder

The database filename is actually specified in the schnack.json configuration file. So this is also a simple change (I changed the template config file as well):

{
    "schnack_host": "https://schnack.example.com",
    "page_url": "https://blog.example.com/posts/%SLUG%",
    "port": 3000,
    "database": {
        "comments": "data/comments.db",
        "sessions": "data/sessions.db"
    },
    "admins": [1],
    "plugins": {
        "auth-twitter": {
            "consumer_key": "xxxxx",
            "consumer_secret": "xxxxx"
        },
        "auth-github": {
            "client_id": "xxxxx",
            "client_secret": "xxxxx"
        },
        "auth-google": {
            "client_id": "xxxxx",
            "client_secret": "xxxxx"
        },
        "auth-facebook": {
            "client_id": "xxxxx",
            "client_secret": "xxxxx"
        },
        "auth-mastodon": {
            "app_name": "your website name",
            "app_website": "https://blog.example.com/"
        },
        "notify-pushover": {
            "app_token": "xxxxx",
            "user_key": "xxxxx"
        },
        "notify-webpush": {
            "vapid_public_key": "xxxxx",
            "vapid_private_key": "xxxxx"
        },
        "notify-slack": {
            "webhook_url": "xxxxx"
        },
        "notify-sendmail": {
            "to": "admin@blog.example.com",
            "from": "schnack@blog.example.com"
        }
    },
    "oauth": {
        "secret": "xxxxx"
    },
    "date_format": "MMMM DD, YYYY - h:mm a"
}

Also done.

Plug-in Architecture

Schnack also designed a very simple plug-in system, which is used to extend itself with different kinds of social network login mechanisms. It supports Google, Facebook, Twitter, Github, etc. to name a few.

However, getting the Plugins to work also means that additional codes need to be fetched in. Hence, I built a simple mechanism to allow the Docker image to install the plugins before starting up. It involves 2 things:

Changing how the software is invoked in the Docker, in Dockerfile.

# Dockerfile
FROM node:lts-buster

WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm install

COPY . .

EXPOSE 3000
CMD ./init.sh
#CMD [ "npm", "run", "server" ]  <-- This is the original invocation method

And I introduced init.sh so that it can be used to install plugins before starting up. Codes below:

#!/bin/bash

# Use npm install @schnack/plugin-auth-github to install additional plugins. Plugin list can be found here:
# https://github.com/schn4ck/schnack-plugins

if [ -f config/plugins ]
then
	source config/plugins
fi

npm run server

It is very simple, it tries to find the plugins file and it can be used to run npm commands within it to install the plugins, per Schnack's recommendations.

Wrapping it up

Let's summarise what I have been doing, and see where we are:

  1. Changed the Dockerfile so that it is built with a more recent node.js image.
  2. Changed the code architecture to split code, configuration and data, and
  3. Added some codes to allow plugins to work nicely with Docker.

All changes are commited to my github repository and a pull request had been submitted to Schnack for acceptance. Also in the middle of pushing the Docker images to Docker Hub.

Now I just need to make it work with my Kubernetes cluster and try it out...

Readout