Deploying HedgeDoc to Dokku

ยท

11 min read

๐Ÿ’ก
This applies to HedgeDoc version 1.x, which is the stable version as of December 2023.

HedgeDoc is an open-source, web-based, self-hosted, collaborative markdown editor. I stumbled upon it while looking for a self-hosted markdown editor. I particularly like it for it's simple and straightforward UI and wide array of features, such as

  • diagrams and charts

  • media embeds / uploads

  • presentations, powered by reveal.js

Dokku is a platform-as-a-service (PaaS) solution that allows you to easily deploy and manage applications on your own infrastructure. It is an open-source alternative to platforms like Heroku, and is designed to be lightweight and straightforward, making it easy for developers to set up and use.

I already have a Dokku instance on an arm64 VPS provided by Hetzner. If you are new to Dokku and would like to give it a spin, please see the official docs. You might also want to have a look at this script I wrote to bootstrap an Ubuntu VPS server prior to installation of Dokku (Use the arm64 branch if you are setting up on an arm64 machine).

I only recently started using servers based on the arm64 architecture, as they have low power consumption and high energy efficiency, which makes them cheaper compared to their x86 counterparts.

Heroku buildpack approach

Dokku has this concept of builders โ€“ which are a way of customizing how an app is built from a source. There are several built-in builders available. However, by default, Dokku normally uses Heroku buildpacks (via Herokuish) for deployment.

โš 
It is important to note that many buildpacks either download pre-compiled things for x86 or only target x86. See this Herokuish issue for details. Therefore, by default, the herokuish builder is disabled on arm/arm64 servers.

Now, before I switched to arm64, I had an x86 Dokku server with HedgeDoc and other apps running on it. I had deployed HedgeDoc using the Heroku buildpacks approach, and it worked very well. How did I do this? Well, in a nutshell, I

  • forked the https://github.com/hedgedoc/hedgedoc repository to https://github.com/engineervix/hedgedoc

  • switched to the master branch, and from there created a dokku branch, where I made some small changes (please see this commit for the details) โ€“ addition of a Procfile and installation of pm2, although the latter isn't necessary.

  • deployed the dokku branch as follows (all commands are executed on the server, unless otherwise indicated)

      # create app
      dokku apps:create hedgedoc
    
      # add a domain to your app
      dokku domains:add hedgedoc example.com
    
      # setup postgres | https://github.com/dokku/dokku-postgres
      # (feel free to use your preferred database backend)
      dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres
      # feel free to choose the image & image version of your choice here
      dokku postgres:create postgres-hedgedoc --image "postgres" --image-version "13.4"
      dokku postgres:link postgres-hedgedoc hedgedoc
      # Take note of the DATABASE_URL, which you will need to assign to CMD_DB_URL later
    
      # set up authentication for backups on the postgres service
      # Datastore backups are supported via AWS S3 and S3 compatible services
      # like https://github.com/minio/minio and https://www.backblaze.com/b2/cloud-storage.html
      # dokku postgres:backup-auth <service> <aws-access-key-id> <aws-secret-access-key> <aws-default-region> <aws-signature-version> <endpoint-url>
      dokku postgres:backup-auth postgres-hedgedoc aws-access-key-id aws-secret-access-key us-west-004 v4 https://s3.us-west-004.backblazeb2.com
      # postgres:backup postgres-hedgedoc <bucket-name>
      dokku postgres:backup postgres-hedgedoc bucket-name
      # everyday at 2:45AM, 10:45AM, 6:45PM
      dokku postgres:backup-schedule postgres-hedgedoc "45 2,10,18 * * *" bucket-name
    
      # Persistent Storage <https://dokku.com/docs/advanced-usage/persistent-storage/>
      # Create a persistent storage directory in the recommended storage path
      dokku storage:ensure-directory --chown heroku hedgedoc
      # Create a new bind mount
      dokku storage:mount hedgedoc /var/lib/dokku/data/storage/hedgedoc/uploads:/home/node/app/public/uploads
    
      # set env variables for your app
      dokku config:set --no-restart hedgedoc CMD_DOMAIN=example.com && \
      dokku config:set --no-restart hedgedoc CMD_ALLOW_ORIGIN='["example.com"]' && \
      dokku config:set --no-restart hedgedoc CMD_SESSION_SECRET=somethingsecret && \
      dokku config:set --no-restart hedgedoc CMD_PROTOCOL_USESSL=true && \
      dokku config:set --no-restart hedgedoc CMD_PORT=3000 && \
      dokku config:set --no-restart hedgedoc CMD_ALLOW_ANONYMOUS=false && \
      dokku config:set --no-restart hedgedoc CMD_ALLOW_EMAIL_REGISTER=false && \
      # NOTE: set CMD_DB_URL to whatever the value of DATABASE_URL is
      dokku config:set hedgedoc CMD_DB_URL=...
    
      # configure buildpacks
      dokku buildpacks:add hedgedoc https://github.com/heroku/heroku-buildpack-nodejs.git
    
      # Customize Nginx | set `client_max_body_size`, to make upload feature work better, for example
      dokku nginx:set hedgedoc client-max-body-size 50m
      # regenerate config
      dokku proxy:build-config hedgedoc
    
      # ports
      dokku ports:add hedgedoc http:80:3000 && \
      dokku ports:add hedgedoc https:443:3000 && \
      dokku ports:remove hedgedoc http:80:5000 && \
      dokku ports:remove hedgedoc https:443:5000
    
      # letsencrypt
      dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
      dokku config:set --no-restart --global DOKKU_LETSENCRYPT_EMAIL=user@example.com
      dokku letsencrypt:set hedgedoc email user@example.com
      dokku letsencrypt:enable hedgedoc
      # this would setup a cron job to renew letsencrypt certificate
      dokku letsencrypt:cron-job --add
    
      # (on local machine) add a remote `dokku` to your git repo, and deploy ๐Ÿš€
      # git remote add dokku dokku@your-server:hedgedoc
      # git push dokku dokku:master
    
      # after successful deployment, you can add a user
      dokku run hedgedoc bin/manage_users --add user@email.com
    

This worked pretty well, until the fire nation attacked I switched to an arm64 server, where I encountered issues during the deployment, because the Heroku buildpack for Node.js did not work on arm64. I therefore had to consider another approach.

Dockerfile approach

At the time of writing this post, HedgeDoc has a Docker image, which doesn't support the arm64 architecture. However, the good folks at LinuxServer.io have created an Alpine-based multi-arch container image which supports arm64.

I tried two approaches:

  1. deploy the linuxserver.io image to my Dokku instance using the Docker Image Deployment approach. I failed to make it work.

  2. create my own Dockerfile based on the above image, and deploy via the Dockerfile deployment approach. After many desperate attempts, I still couldn't get things to work.

I ended up giving up. This was in October 2023. The main issue I encountered was that when I ran the application, I couldn't connect to the database, as the application somehow couldn't read the environment variables I had set.

Two months later, I decided to revisit the problem, because I really missed HedgeDoc, having become accustomed to it after previously using it for about a year. I couldn't find any suitable replacement and I was getting frustrated.

I realised that the linuxserver.io image was primarily meant to be deployed via docker compose, so I decided to focus on creating my own Dockerfile, where I would have more control.

This Stack Overflow post was my eureka moment! It seems the environment variables I set (via dokku config:set hedgedoc KEY=VALUE) were not available at build time for non-buildpack-based deploys. According to the Dokku docs:

Environment variables are available both at run time and during the application build/compilation step for buildpack-based deploys.

Even before I stumbled upon the aforementioned Stack Overflow post, I was setting build-time configuration variables (via dokku docker-options:add hedgedoc build '--build-arg KEY=VALUE'), but things were still broken.

What was missing is using both ARG and ENV in the Dockerfile (I was only using ENV), like this:

ARG CMD_DOMAIN
ENV CMD_DOMAIN=${CMD_DOMAIN}

And with this, I was finally able to have a working HedgeDoc deployment ๐ŸŽ‰!

So, what does the final code look like? Well, here's a diff between the master branch on the official HedgeDoc repository and the dokku branch on my clone (click on the Files changed tab). The key changes are:

  • addition of a Dockerfile

      FROM node:18.18.2-bullseye
    
      RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
    
      # Set the working directory in the container
      WORKDIR /home/node/app
    
      # Port used by this container to serve HTTP.
      EXPOSE 3000
    
      # set environment variables
      # reference: https://docs.hedgedoc.org/configuration/
      ARG CMD_DOMAIN \
          CMD_SESSION_SECRET \
          CMD_ALLOW_ORIGIN \
          CMD_IMAGE_UPLOAD_TYPE \
          CMD_DB_URL
      ENV PORT=3000 \
          CMD_PORT=3000 \
          CMD_IMAGE_UPLOAD_TYPE=${CMD_IMAGE_UPLOAD_TYPE} \
          CMD_ALLOW_ANONYMOUS=false \
          CMD_PROTOCOL_USESSL=true \
          CMD_URL_ADDPORT=false \
          CMD_ALLOW_EMAIL_REGISTER=false \
          CMD_DB_DIALECT=postgres \
          NODE_ENV=production \
          CMD_DOMAIN=${CMD_DOMAIN} \
          CMD_SESSION_SECRET=${CMD_SESSION_SECRET} \
          CMD_ALLOW_ORIGIN=${CMD_ALLOW_ORIGIN} \
          CMD_DB_URL=${CMD_DB_URL} \
          # necessary on ARM because puppeteer doesn't provide a prebuilt binary
          PUPPETEER_SKIP_DOWNLOAD=true
    
      # Yarn 3 requires corepack to be enabled
      RUN corepack enable
    
      # Switch to the non-root user
      USER node
    
      # Copy the code to the container
      COPY --chown=node:node . .
      COPY --chown=node:node config.json.example ./config.json
    
      # Remove the "saml" section from the config.json file
      # because of https://community.hedgedoc.org/t/change-certificate-file-path-of-idp-in-pem-format/108/3
      RUN sed -i '/"saml": {/,/},/d' ./config.json
    
      # Install the application dependencies
      RUN yarn workspaces focus --production
    
      # # Build the frontend bundle
      RUN yarn install --immutable && \
          yarn run build
    
      # Runtime command that executes when "docker run" is called
      # do nothing - exec commands elsewhere
      CMD tail -f /dev/null
    
  • addition of a Procfile

      web: yarn start
    
  • addition of a .dockerignore file

      .git
      .gitignore
      node_modules
      build
      Dockerfile
      .dockerignore
      .gitignore
      .env
    

As part of the troubleshooting process, I did also modify some files:

  • lib/config/default.js

      -  dbURL: '',
      +  dbURL: process.env.DATABASE_URL || '',
    
  • lib/models/index.js

        if (config.dbURL) {
      +    logger.info(config.dbURL)
          sequelize = new Sequelize(config.dbURL, dbconfig)
        } else {
      +    logger.warn('config.dbURL is not set')
          sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig)
        }
    

I ended up leaving those modifications (I was probably tired and just wanted to use my HedgeDoc!), you don't need to make them, though they might be useful.

Here is how I deployed to Dokku (again, all commands are executed on the server, unless otherwise indicated).

# create app
dokku apps:create hedgedoc

# add a domain to your app
dokku domains:add hedgedoc example.com

# setup postgres | https://github.com/dokku/dokku-postgres
# (feel free to use your preferred database backend)
dokku plugin:install https://github.com/dokku/dokku-postgres.git postgres
# feel free to choose the image & image version of your choice here
dokku postgres:create postgres-hedgedoc --image "postgres" --image-version "13.4"
dokku postgres:link postgres-hedgedoc hedgedoc
# Take note of the DATABASE_URL, which you will need to assign to CMD_DB_URL later

# set up authentication for backups on the postgres service
# Datastore backups are supported via AWS S3 and S3 compatible services
# like https://github.com/minio/minio and https://www.backblaze.com/b2/cloud-storage.html
# dokku postgres:backup-auth <service> <aws-access-key-id> <aws-secret-access-key> <aws-default-region> <aws-signature-version> <endpoint-url>
dokku postgres:backup-auth postgres-hedgedoc aws-access-key-id aws-secret-access-key us-west-004 v4 https://s3.us-west-004.backblazeb2.com
# postgres:backup postgres-hedgedoc <bucket-name>
dokku postgres:backup postgres-hedgedoc bucket-name
# everyday at 2:45AM, 10:45AM, 6:45PM
dokku postgres:backup-schedule postgres-hedgedoc "45 2,10,18 * * *" bucket-name

# Persistent Storage <https://dokku.com/docs/advanced-usage/persistent-storage/>
# Create a persistent storage directory in the recommended storage path
dokku storage:ensure-directory --chown heroku hedgedoc
# Create a new bind mount
dokku storage:mount hedgedoc /var/lib/dokku/data/storage/hedgedoc/uploads:/home/node/app/public/uploads

# set env variables for your app
# I wonder whether this is even necessary, since we are actually using Docker build args
dokku config:set --no-restart hedgedoc CMD_DOMAIN=example.com && \
dokku config:set --no-restart hedgedoc CMD_ALLOW_ORIGIN='["example.com"]' && \
dokku config:set --no-restart hedgedoc CMD_SESSION_SECRET=somethingsecret && \
dokku config:set --no-restart hedgedoc CMD_IMAGE_UPLOAD_TYPE=filesystem && \
# NOTE: set CMD_DB_URL to whatever the value of DATABASE_URL is
dokku config:set hedgedoc CMD_DB_URL=...

# customize Docker Build-time configuration variables
# https://dokku.com/docs/deployment/builders/dockerfiles/#build-time-configuration-variables
# --------------------------------------------------------------------------------------------
dokku docker-options:add hedgedoc build '--build-arg CMD_DOMAIN=example.com' && \
dokku docker-options:add hedgedoc build '--build-arg CMD_ALLOW_ORIGIN=["example.com"]' && \
dokku docker-options:add hedgedoc build '--build-arg CMD_SESSION_SECRET=somethingsecret' && \
dokku docker-options:add hedgedoc build '--build-arg CMD_IMAGE_UPLOAD_TYPE=filesystem' && \
dokku docker-options:add hedgedoc build '--build-arg CMD_DB_URL=postgres://username:password@host:port/database'
# add others based on your requirements. See https://docs.hedgedoc.org/configuration
# NOTE: I just saw [from the docs](https://dokku.com/docs/deployment/builders/dockerfiles/#build-time-configuration-variables) as I was writing this, that:
# "All environment variables set by the config plugin are automatically exported during a docker build,
#  and thus --build-arg only requires setting a key without a value.
#
#  dokku docker-options:add node-js-app build '--build-arg NODE_ENV'
# "

# Customize Nginx | set `client_max_body_size`, to make upload feature work better, for example
dokku nginx:set hedgedoc client-max-body-size 50m
# regenerate config
dokku proxy:build-config hedgedoc

# ports
dokku ports:add hedgedoc http:80:3000 && \
dokku ports:add hedgedoc https:443:3000 && \
dokku ports:remove hedgedoc http:80:5000 && \
dokku ports:remove hedgedoc https:443:5000

# letsencrypt
dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku config:set --no-restart --global DOKKU_LETSENCRYPT_EMAIL=user@example.com
dokku letsencrypt:set hedgedoc email user@example.com
dokku letsencrypt:enable hedgedoc
# this would setup a cron job to renew letsencrypt certificate
dokku letsencrypt:cron-job --add

# (on local machine) add a remote `dokku` to your git repo, and deploy ๐Ÿš€
# git remote add dokku dokku@your-server:hedgedoc
# git push dokku dokku:master

# after successful deployment, you can add a user
dokku run hedgedoc bin/manage_users --add user@email.com

You will observe that the commands executed above are not so different from the ones for the Heroku buildpack approach. The main differences:

Heroku buildpack approachDockerfile approach
We specify a buildpack (dokku buildpacks:add hedgedoc https://github.com/heroku/heroku-buildpack-nodejs.git).We do not specify a buildback. Because the repo has a Dockerfile, Dokku will detect it and use the builder-dockerfile.
No need to specify build time configuration variables, because environment variables are available both at run time and during the application build/compilation step for buildpack-based deploys.We have to specify build time configuration variables.
๐Ÿ’ก
You may have noticed from the git diff that I specified a couple of CMDS3 variables and other CMDDB variables in the Dockerfile, which I have not defined on the server. I initially wanted to use an S3-compatible cloud storage for uploads. Having configured a private bucket on Backblaze, I was able to upload files, however, I couldn't preview them, due to the way Backblaze handles CORS on private buckets. So I resorted to using the filesystem (I plan to configure automated backups via rclone). Regarding the CMDDB* variables, if CMD_DB_URL is set, then we really do not need the rest. I only set these as I was troubleshooting database connection issues.

Well, I hope you find this useful. If you have any suggestions, questions or comments, please feel free to comment below. Enjoy your HedgeDoc!

Did you find this article valuable?

Support Victor's Blog by becoming a sponsor. Any amount is appreciated!

ย