# Deploying HedgeDoc to Dokku

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This applies to HedgeDoc version 1.x, which is the stable version as of December 2023.</div>
</div>

[HedgeDoc](https://hedgedoc.org/) 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](https://revealjs.com/)
    

[Dokku](https://dokku.com/) 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](https://www.heroku.com/), 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](https://www.hetzner.com/press-release/arm64-cloud). If you are new to Dokku and would like to give it a spin, please see [the official docs](https://dokku.com/docs/getting-started/installation/). You might also want to have a look at [this script](https://github.com/engineervix/pre-dokku-server-setup) I wrote to bootstrap an Ubuntu VPS server prior to installation of Dokku (Use the [`arm64`](https://github.com/engineervix/pre-dokku-server-setup/tree/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](https://www.lenovo.com/us/en/glossary/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](https://dokku.com/docs/deployment/builders/builder-management/#builder-selection). However, by default, Dokku normally uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) (via [Herokuish](https://github.com/gliderlabs/herokuish#buildpacks)) for deployment.

<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">It is important to note that many buildpacks either download pre-compiled things for x86 or only target x86. See <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/gliderlabs/herokuish/issues/87" style="pointer-events: none">this Herokuish issue</a> for details. Therefore, by default, the herokuish builder <a target="_blank" rel="noopener noreferrer nofollow" href="https://dokku.com/blog/2022/dokku-0.29.0/#initial-herokuish-support-on-arm-servers" style="pointer-events: none">is disabled on arm/arm64 servers</a>.</div>
</div>

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](https://github.com/hedgedoc/hedgedoc) repository to [https://github.com/engineervix/hedgedoc](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](https://github.com/engineervix/hedgedoc/commit/0a8385ed54a5057e7d345ecb7f8700c0c2d871b7) for the details) – addition of a [`Procfile`](https://devcenter.heroku.com/articles/procfile) and installation of [`pm2`](https://pm2.keymetrics.io/), although the latter isn't necessary.
    
* deployed the `dokku` branch as follows (all commands are executed on the server, unless otherwise indicated)
    
    ```bash
    # 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 [<s>the fire nation attacked</s>](https://knowyourmeme.com/memes/everything-changed-when-the-fire-nation-attacked) I switched to an arm64 server, where I encountered issues during the deployment, because the [Heroku buildpack for Node.js](https://github.com/heroku/heroku-buildpack-nodejs) [did not work on arm64](https://github.com/heroku/heroku-buildpack-nodejs/issues/964). I therefore had to consider another approach.

## Dockerfile approach

At the time of writing this post, HedgeDoc [has a Docker image](https://docs.hedgedoc.org/setup/docker/), which doesn't support the arm64 architecture. However, the good folks at [LinuxServer.io](https://linuxserver.io) have created an [Alpine-based multi-arch container image](https://docs.hedgedoc.org/setup/community/#linuxserverio-docker-image) which supports arm64.

I tried two approaches:

1. deploy the [linuxserver.io image](https://github.com/linuxserver/docker-hedgedoc) to my Dokku instance using the [Docker Image Deployment approach](https://dokku.com/docs/deployment/methods/image/). I failed to make it work.
    
2. create my own Dockerfile based on the above image, and deploy via the [Dockerfile deployment approach](https://dokku.com/docs/deployment/builders/dockerfiles/). After [many desperate attempts](https://github.com/engineervix/hedgedoc/commits/dokku/?author=engineervix&since=2023-10-01&until=2023-10-31), 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](https://docs.hedgedoc.org/configuration/) 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](https://github.com/linuxserver/docker-hedgedoc) was [primarily meant to be deployed via docker compose](https://github.com/linuxserver/docker-hedgedoc#docker-compose-recommended-click-here-for-more-info), so I decided to focus on creating my own Dockerfile, where I would have more control.

[This Stack Overflow post](https://stackoverflow.com/questions/63277210/get-env-variables-with-dockerized-vue-app-in-dokku) 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](https://dokku.com/docs/configuration/environment-variables/):

> 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](https://dokku.com/docs/deployment/builders/dockerfiles/#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:

```dockerfile
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](https://github.com/hedgedoc/hedgedoc/compare/master...engineervix:hedgedoc:dokku?expand=1) 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`
    
    ```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`
    
    ```plaintext
    web: yarn start
    ```
    
* addition of a `.dockerignore` file
    
    ```plaintext
    .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`
    
    ```diff
    -  dbURL: '',
    +  dbURL: process.env.DATABASE_URL || '',
    ```
    
* `lib/models/index.js`
    
    ```diff
      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).

```bash
# 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`](https://dokku.com/docs/deployment/builders/dockerfiles/). |
| 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. |

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You may have noticed from the git diff that I specified a couple of <code>CMD_S3_*</code> variables and other <code>CMD_DB_*</code> variables in the <code>Dockerfile</code>, which I have not defined on the server. I initially wanted to use an <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.backblaze.com/" style="pointer-events: none">S3-compatible cloud storage</a> for uploads. Having configured a private bucket on <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.backblaze.com/" style="pointer-events: none">Backblaze</a>, I was able to upload files, however, I couldn't preview them, due to <a target="_blank" rel="noopener noreferrer nofollow" href="https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules#cors-on-private-buckets" style="pointer-events: none">the way Backblaze handles CORS on private buckets</a>. So I resorted to using the filesystem (I plan to configure automated backups via <a target="_blank" rel="noopener noreferrer nofollow" href="https://rclone.org" style="pointer-events: none">rclone</a>). Regarding the <code>CMD_DB_*</code> variables, if <code>CMD_DB_URL</code> is set, then we really do not need the rest. I only set these as I was troubleshooting database connection issues.</div>
</div>

---

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