Deploying HedgeDoc to Dokku

Building, breaking and fixing things. Learning something new everyday.
Search for a command to run...

Building, breaking and fixing things. Learning something new everyday.
No comments yet. Be the first to comment.
It's About Time

A developer's journey through CV formats and the creation of a streamlined Markdown-based solution

Nautilus or "Files" is the default file manager that ships with GNOME. The stock context menu entries are sufficient for general purposes. However, there are times when you want the ability to add other entries to enable you perform custom actions. I...

From Flatpak to RPM โ A Journey in Linux Package Development

Dependency management is a crucial aspect of any project. Keeping your dependencies up to date ensures security, stability, and access to the latest features. However, simply running poetry update can be risky. What if you have 25 outdated packages, ...

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.
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.
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.
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:
deploy the linuxserver.io image to my Dokku instance using the Docker Image Deployment approach. I failed to make it work.
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 approach | Dockerfile 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. |
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!