Replacing Disqus with Remark42

Data Privacy Graphic

A while back, I started looking at self-hosted alternatives to Disqus for this blog, mainly since Disqus is slow to load as well as being a privacy nightmare.

For the blog itself I use the excellent static generator hugo and host via netlify. I was looking for something:

  1. self-hosted so I’m not reliant on any third party service
  2. easy to embed with a js snippet

After a bit of research, I eventually settled on remark42 which is pretty full-featured. It’s written in go, so comes as a single binary. They do also provide docker builds, but I’m not interested in that method here.

Here’s how I went about setting it up on my VPS, and embedding it on my blog.

Server Setup with Nginx and LetsEncrypt

I’m currently on Ubuntu 20.04 but this process should be pretty similar for other versions. I’m also assuming we already have e.g. nginx and certbot (and probably other things!) installed. If anything is not clear or incomplete, please let me know and I’ll try to update it.

First of all we grab the remark42 binary from github and make it available from the cli. You’ll need to select the version that’s relevant to your specific server architecture - in my case it’s linux-amd64 - you can run uname -ms if you’re not sure and figure it out from there.

cd bin
gunzip remark42.linux-amd64.tar.gz
tar -xvf remark42.linux-amd64.tar
rm remark42.linux-amd64.tar
mv remark42.linux-amd64 remark42

I had a look at the existing auth options and decided to start with github and anonymous. So next I log into github, set up an app and make a note of the credentials. I’m pretty much just following the instructions in the docs, but I’ve added the minor but important step 4 below.

  1. Create a new “OAuth App”:
  2. Fill in the “Application Name” and “Homepage URL” for your site
  3. Under “Authorization callback URL” enter the correct URL constructed as domain + /auth/github/callback, i.e.,
  4. Click on Generate a new client secret
  5. Make a note of the Client ID (as AUTH_GITHUB_CID) and Client Secret (AUTH_GITHUB_CSEC)

I also created a remark secret and a password to use for the app and saved them in my password manager for use below.

Then we need to use all these credentials to start the application along with the AUTH_ANON option. I’m also using nohup so that the process never ends, and I’ve set it to run via an arbitrary local port.

cd ~
export AUTH_ANON=true && export AUTH_GITHUB_CID=YourGithubAppId && export AUTH_GITHUB_CSEC=YourGithubAppSecret && nohup remark42 server --secret=YourRemarkSecret --url= --port=4200 --admin-passwd=YourRemarkPassword &

Now it’s time to set up nginx

micro /etc/nginx/sites-available/remark42
server {
    listen      443;
    ssl    on;
    ssl_certificate        /etc/letsencrypt/live/;
    ssl_certificate_key    /etc/letsencrypt/live/;

    gzip on;
    gzip_types text/plain application/json text/css application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml;
    gzip_min_length 1000;
    gzip_proxied any;

    location ~ /\.git {
        deny all;

    location /index.html {
         proxy_redirect          off;
         proxy_set_header        X-Real-IP $remote_addr;
         proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header        Host $http_host;
         proxy_pass    ;

    location / {
         proxy_redirect          off;
         proxy_set_header        X-Real-IP $remote_addr;
         proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header        Host $http_host;
         proxy_pass    ;

    access_log   /var/log/nginx/remark42.log;


server {
  listen 80;
  return      301$request_uri;

This is based on the nginx conf boilerplate in the docs but with a few adjustments for letsencrypt, ssl and our local port etc. Obviously you’ll need to replace with your own domain.

Once that’s done, we link it over into sites-enabled so that nginx can see it.

ln -s /etc/nginx/sites-available/remark42 /etc/nginx/sites-enabled/remark42

I now want to go and set up an A record for my new domain over in the dns provider, in my case this is cloudflare.

Once that’s done I then need to stop/start nginx and grab the letsencrypt certs via certbot. I’ve already added their paths to the nginx conf above.

systemctl stop nginx
certbot -d certonly --standalone
systemctl start nginx

I’ve already set up Certbot to renew automatically in my cron so I don’t need to worry about that here, but you might need to set this up here.

You should now be able to visit and see the default view.

Public discussion illustration

Importing Disqus Comments

I then export comments from disqus as per this guide. However as of today’s date the location for the export functionality is actually moderation->tools ->export if you’re struggling to find it!

Now we can push the export file to the server and unzip it - I used scp and gunzip. Then finally we import the comments into remark42. I found that this needs a bunch of extra flags which the docs don’t mention.

remark42 import -p disqus -f ./path/to/your/disqus-export.xml -s remark --secret=YourRemarkSecret --url= --admin-passwd=YourRemarkPassword

Embedding into Hugo

We can now add our js embed code to hugo. I’m using the coder theme for my blog - I extend it with a remark42 footer partial and some parameters to handle the new feature.

<!-- themes/hugo-coder/layouts/partials/posts/remark42.html -->
{{- if and (isset .Site.Params "remark42url") (not (eq .Site.Params.remark42Url "" )) (eq (.Params.disable_comments | default false) false) -}}
  var remark_config = {
    host: '{{ .Site.Params.remark42Url }}',
    site_id: '{{ .Site.Params.remark42Id }}',
    theme: "dark"
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
<div id="remark42"></div>
{{- end -}}

<!-- themes/hugo-coder/layouts/posts/single.html -->
	{{ partial "posts/remark42.html" . }}
# config.toml
    remark42Url = ""
    remark42Id = 'remark'

I also need to add some css to give the comment iframe a minimum height, otherwise it tends to sometimes cut off until you refresh the page.

footer #remark42 iframe {
  min-height: 20em;

I then deploy the site, for me this just means a simple git commit and git push and then waiting a few seconds for netlify to rebuild.

Admin rights

We now want to be able to give our user admin rights. In my case I visited my site, logged in to the commenting with github, then made a note of my user id. If you click on your name, a side panel pops out and you can copy it from there.

We then need to go back to the server command line, find the remark42 process, kill it, and restart it with the ADMIN_SHARED_ID option filled in with this user id.

ps -a
kill [relevant pid]
export AUTH_ANON=true && export AUTH_GITHUB_CID=YourGithubAppId && export AUTH_GITHUB_CSEC=YourGithubAppSecret && export ADMIN_SHARED_ID=YourUserId && nohup remark42 server --secret=YourRemarkSecret --url= --port=4200 --admin-passwd=YourRemarkPassword &

All done, I now have moderation permissions… If you’re the paranoid/security conscious type like me, you could also think about editing your bash history and deleting/obfuscating the various credentials.

Next Up

There’s probably more to do to get it fully set up as I’d like - for example I would maybe remove the anonymous login and enable some of the other options, especially email.

It would also be really cool if remark42 had some way of logging in with a fediverse account and/or IndieAuth. @[email protected] pointed me to this idea, which is to link a blog post to a fedi post and then use replies as comments - nice concept although slightly different paradigm.

Will leave that to another day to explore those things further!