April 30, 2021

Standard Notes, Docker-Compose & Caddy V2

Standard Notes is a free & open source, end-to-end-encrypted notes app available for desktop, mobile & web.

This guide is for those looking to take digital privacy & data ownership to the next level by self hosting the Standard Notes sync server, web client and extensions.

We'll cover installing Standard Notes on Ubuntu 20.04 using Docker Compose with Caddy V2 as reverse proxy.

We'll also cover one way to automate backing up your Standard Notes database to a Nextcloud instance using Python!

We assume you have:

  • Basic Linux / sysadmin skills
  • Ubuntu 20.04 with sudo
  • Access to a domain name

Note: At the time of this writing, Standard Notes is built on Ruby and Ruby on Rails but is currently undergoing a refactoring with TypeScript and JavaScript. Keep an eye out for a new guide once the new version is released!

Docker Compose

Please be sure the latest Docker and Docker Compose are correctly installed.

To keep things as simple and easy to work with as possible, we'll be combining the three core components into a single docker compose file so everything is in one place and can be started and stopped with a single command.

You can store these anywhere, in this guide we'll use /home/YourUser/Docker. Go ahead and create the file:

cd /home/YourUser/Docker && mkdir standard-notes-server
cd standard-notes-server && sudo nano docker-compose.yml

Paste in the following and note the changes to be made:

version: '3'

services:
  mysql:
    image: mysql:latest
    security_opt:
      - "seccomp:unconfined"
    container_name: Standard-Notes-MySQL
    environment:
      MYSQL_ROOT_PASSWORD: ChangeMeNow
      MYSQL_DATABASE: standard_notes_db
      MYSQL_USER: std_notes_user
      MYSQL_PASSWORD: ChangeMeMySQLdbPassword
    ports:
      - "3306:3306"
    volumes:
      - ./data:/var/lib/mysql
  standard-notes-server:
    image: standardnotes/syncing-server:stable
    container_name: Standard-Notes-Server
    env_file: server.env
    depends_on:
      - mysql
    ports:
      - 3000:3000
  standard-notes-web:
    image: standardnotes/web:stable
    container_name: Standard-Notes-Web
    env_file: web.env
    ports:
      - 3001:3001

Important: make sure that you generate a unique strong random password and enter one in each of the MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD fields. Make sure you keep track of these passwords, Bitwarden is an excellent choice for that. We'll need to enter some of these same passwords again shortly.

Next, create a file in the same directory called server.env:

sudo nano server.env

Paste in the following, and note the few changes to be made:

# Environment variables for Standard Notes sync server

# Rails Settings
EXPOSED_PORT=3000
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=false
RAILS_LOG_LEVEL=INFO
ACTIVE_JOB_QUEUE_ADAPTER=async

# Database Settings
DB_PORT=3306
DB_HOST=mysql
DB_DATABASE=standard_notes_db
DB_USERNAME=std_notes_user
DB_PASSWORD=ChangeMeMySQLdbPassword
DB_POOL_SIZE=30
DB_WAIT_TIMEOUT=180

SECRET_KEY_BASE=ChangeMeYetAnotherUniquePassword
PSEUDO_KEY_PARAMS_KEY=ChangeMeYesYetAnotherHere

# Uncomment below to disable new registrations:
#DISABLE_USER_REGISTRATION=true

# Datadog
DATADOG_ENABLED=false

# Revisions persistency
REVISIONS_FREQUENCY=300

# (Optional) Change URLs to Internal DNS
INTERNAL_DNS_REROUTE_ENABLED=false

# (Optional) Auth Proxy JWT Secret
AUTH_JWT_SECRET=ChangeMeCreateANOTHERuniquePassword

Important: be sure to change the following in server.env above:

  • DB_PASSWORD must match MYSQL_PASSWORD
  • MYSQL_ROOT_PASSWORD must be a different password
  • SECRET_KEY_BASE, PSEUDO_KEY_PARAMS_KEY and AUTH_JWT_SECRET must all be unique passwords

Great, now let's create another file, again in the same directory, this time called web.env:

sudo nano web.env

Paste in the following, again note the changes to be made:

# Environment variables for Standard Notes web client

RAILS_ENV=production
PORT=3001
WEB_CONCURRENCY=0
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
SECRET_KEY_BASE=CreateAnotherUniquePasswordHere
APP_HOST=https://notes.yourdomain.com

EXTENSIONS_MANAGER_LOCATION=extensions/extensions-manager/dist/index.html
BATCH_MANAGER_LOCATION=extensions/batch-manager/dist/index.min.html
SF_DEFAULT_SERVER=https://sync.yourdomain.com

# Datadog
DATADOG_ENABLED=false

# Development options
DEV_DEFAULT_SYNC_SERVER=https://sync.yourdomain.com
DEV_EXTENSIONS_MANAGER_LOCATION=public/extensions/extensions-manager/dist/index.html
DEV_BATCH_MANAGER_LOCATION=public/extensions/batch-manager/dist/index.min.html

Important: change the following in web.env above:

  • SECRET_KEY_BASE yes, anther unique password
  • APP_HOST where to serve up web client, something like notes.yourdomain.com
  • SF_DEFAULT_SERVER the sync server, something like sync.yourdomain.com
  • DEV_DEFAULT_SYNC_SERVER sync server again, sync.yourdomain.com

Important: your domains sync.yourdomain.com and notes.yourdomain.com must point to your server's IP address.

Make sure you're still in /home/YourUser/Docker/standard-notes-server/, then go ahead and start the containers with:

docker-compose up -d

Next we'll set up Caddy so your instance becomes accessible at your domain.

Install & Configure Caddy

Caddy helps simplify setting up reverse proxy and HTTPS. If you already have this installed from previous guides, you can go ahead and skip over to the configuration part.

To install Caddy:

sudo apt install curl
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Now head over to the Caddyfile:

cd /etc/caddy && sudo nano Caddyfile

You'll need to add two entries here, one for each domain:

notes.yourdomain.com {
    reverse_proxy localhost:3001
    encode gzip
    file_server
}

sync.yourdomain.com {
    reverse_proxy localhost:3000
    encode gzip
    file_server
}

Be sure notes is pointing to 3001 while sync is pointing to 3000, or if you used different port numbers, make sure they match here. After adding that configuration go ahead and type caddy reload from the terminal (make sure you're still in /etc/caddy).

You should now be able to access your Standard Notes web client at https://notes.yourdomain.com! On first run, be sure to go to Advanced Settings as you're trying to register or sign in, and make sure that Sync Server Domain shows your expected https://sync.yourdomain.com there.

Congratulations, you now have a fully functional self hosted Standard Notes instance! You can stop here if you like, or read on for some finishing touches like self hosting Standard Notes extensions and automating backups of your Standard Notes database!

If you do want extensions but don't care to self host that part, click on Extensions from within Standard Notes, and then where it says Enter Your Extended Activation Code, enter https://gitcdn.xyz/cdn/kylejbrk/standard-notes-open-extended/gh-pages/index.json.

To self host the extensions, see the next section.

Self Hosting Standard Notes Extensions

You'll need another domain (or subdomain) for this, we'll go with extensions.yourdomain.com. Be sure to have added it with your domain registrar. You'll also need to create a free Github account if you don't already have one. In Github, go to Settings -> Developer settings -> Personal access tokens and Generate new token. Take note of the key as you'll need to paste it into the configuration next.

Make you're still in the standard-notes-server folder and git clone the extensions in:

cd /home/YourUser/Docker/standard-notes-server/
git clone `https://github.com/iganeshk/standardnotes-extensions`

Rename and then edit the sample environment file:

cd standardnotes-extensions
mv env.sample .env
sudo nano .env

Enter your extensions domain and Github token:

domain: https://extensions.yourdomain.com

github:
  username: YourGithubUsername
  token: YourGithubTokenHere

For the next part you need to make sure that Python3 with requests and pyyaml are installed.

To make sure, do:

sudo apt install python3 python3-pip
pip3 install requests
pip3 install pyyaml 

Now you can build the extensions repo with:

python3 build_repo.py

This will create a public subdirectory with all the extensions in it. Set permissions for public:

sudo chown -R root:root public
sudo chmod -R 750 public

Next, edit the Caddyfile:

cd /etc/caddy && sudo nano Caddyfile

Add the configuration for your extensions domain:

extensions.yourdomain.com {
    root * /home/YourUser/Docker/standard-notes-server/standardnotes-extensions/public
    header {
        Access-Control-Allow-Origin *
        Access-Control-Allow-Methods "POST, GET, OPTIONS"
        Access-Control-Allow-Headers "*"
    }
    encode gzip
    file_server
}

Now do caddy reload. To confirm the extensions are now accessible, go to https://extensions.yourdomain.com/index.json and this should return a JSON list of extensions. If so, great! You can go ahead and paste https://extensions.yourdomain.com/index.json into Standard Notes -> Extensions -> Enter Your Extended Activation Code. You can now install and activate your self hosted extensions!

Delete Users

Standard Notes doesn't come with an admin panel just yet, so if you ever need to delete a user, you can do so by issuing a POST request to the admin API. To do that, first add the following to the server.env file:

ADMIN_IPS=192.168.X.XX,XX.XX.XX.XX
ADMIN_KEY=GenerateAStrongPasswordHere

After making those changes, do docker-compose up -d to recreate the container.

ADMIN_IPS is a list of IPs to whitelist (wherever you'll be connecting from). You can just stick to the local IP unless you'll need to access it from outside. To delete a user, do this command:

curl -X POST -d 'admin_key=YourAdminKeyHere&[email protected]' http://192.168.X.XX:3000/admin/delete_account?

If you'll be accessing it from a public/WAN IP instead, be sure to include https and to omit the port number (ie, https://sync.yourdomain.com/admin/delete_account?).

In the final part of this guide, we'll cover how to use a small Python script to automate backups of your Standard Notes database to a Nextcloud instance.

Automate Standard Notes Backups to Nextcloud

This script will use webdavclient. Please make sure it's installed:

pip3 install webdavclient

You will also need a Nextcloud App Password. Open Nextcloud, go to Settings -> Personal -> Security, scroll to the bottom and enter an app name like standard-notes-backup and click Create new app password. Take note of it to copy into the script below.

You can store your Python scripts wherever you like, we'll use /home/YourUser/Scripts/Python. Create the Python file and paste in the code:

cd /home/YourUser && mkdir Scripts
cd Scripts && sudo nano standard-notes-backup.py
import webdav.client, os, time

# configure & initialize webdav client
webdavconfig = {'webdav_hostname': "https://cloud.yourdomain.com/remote.php/dav/files/YourUser/", 'webdav_login': "YourUser", 'webdav_password': "NextcloudAppPassword", 'webdav_root': "/"}
wc = webdav.client.Client(webdavconfig)

# set datestamp and file paths
datestamp = time.strftime("%Y-%m-%d")
sourcefiles = '/var/lib/docker/volumes/standard-notes-server_dbdata'
targetfiles = f'/Backup/standard-notes-backup/{datestamp}/'

# stop Standard Notes containers
os.chdir('/home/server/Docker/standard-notes-server')
os.system('docker-compose stop')

# make sure the destination folders exist
if not wc.check("Backup"):
    wc.mkdir("Backup")
if not wc.check("Backup/standard-notes-backup"):
    wc.mkdir("Backup/standard-notes-backup")
if not wc.check(f"Backup/standard-notes-backup/{datestamp}"):
    wc.mkdir(f"Backup/standard-notes-backup/{datestamp}")

# upload the data to Nextcloud
wc.upload_sync(remote_path=targetfiles, local_path=sourcefiles)

# start Standard Notes containers
os.system('docker-compose start')

Do a test run to make sure the script works as intended:

python3 standard-notes-backup.py

Check Nextcloud to confirm the files appeared in Backup/standard-notes-backup. If so, great! The last step is to set a cronjob to have the script run daily.

sudo -i
crontab -e

Add this line at the bottom:

0 4 * * * /usr/bin/python3 /home/YourUser/Scripts/Python/standard-notes-backup.py >/tmp/standard-notes-cron.log 2>&1

This will run your Python script daily at 4am with any errors being stored in a log at /tmp/standard-notes-cron.log.

We hope you found this guide helpful. Please share any feedback you might have on our contact page! Thanks for reading.