Watchtower: Automate Docker Container Updates
Virtualization, and containerization in particular have been an incredible boon in technology over the past
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:
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!
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 passwordSECRET_KEY_BASE
, PSEUDO_KEY_PARAMS_KEY
and AUTH_JWT_SECRET
must all be unique passwordsGreat, 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 passwordAPP_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.
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.
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!
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=UserToDelete@domain.com' 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.
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.