Zn
Applications Azure

Zen Notes on Ubuntu 24.04 on Azure User Guide

| Product: Zen Notes on Ubuntu 24.04 LTS on Azure

Overview

Zen (published here as Zen Notes to disambiguate the generic upstream name) is an open source, self-hosted notes application built as a single Go binary. Every note is stored as standard Markdown inside a local SQLite database, with full-text search (BM25 ranking), flexible tags instead of rigid folders, templates, pinned and archived notes, and image uploads - all in a fast, low-resource web app. The cloudimg image delivers Zen fully built and reverse-proxied with nginx over HTTPS: the Zen server is a single self-contained binary bound to the loopback interface, and nginx terminates TLS in front of it. The database and uploads live on a dedicated Azure data disk mounted at /var/lib/zen, separate from the OS disk and re-provisioned with every VM. Backed by 24/7 cloudimg support.

What is included:

  • Zen 1.5.0 (built from source, AGPL-3.0) as a single Go binary at /usr/local/bin/zen, bound to loopback 127.0.0.1:8080
  • nginx terminating TLS on :443 in front of the Zen server, with :80 redirecting to HTTPS
  • A dedicated Azure data disk at /var/lib/zen holding the SQLite database (zen.db) and uploaded images - separate from the OS disk and independently resizable
  • A per-VM administrator account generated on first boot - no default or shared credential ships in the image
  • zen.service + nginx.service as systemd units, enabled and active
  • 24/7 cloudimg support

Secure by default - a per-VM administrator credential

Zen is a single-user application. This image ships with no default or shared login: the SQLite database is shipped empty of user accounts, and on first boot a one-shot service generates a unique administrator password for your VM, hashes it, seeds exactly one admin account (admin@zen.local), and writes the credential to a root-only file. The Zen server is deliberately held back until that account exists, so there is never a window in which a fresh VM is reachable without an administrator. You retrieve the password over SSH and change it after your first login.

Zen Notes login page served over HTTPS

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point; scale up for large note or image volumes. NSG inbound: allow 22/tcp from your management network, and 80/tcp + 443/tcp from wherever you browse Zen (:80 only redirects to :443).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Zen Notes by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443). Review the dedicated data disk on the Disks tab, then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name zen-notes \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name zen-notes --port 80 --priority 1010
az vm open-port --resource-group <your-rg> --name zen-notes --port 443 --priority 1020

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

The message of the day shows your VM's Zen URL and administrator email.

Step 4 - Confirm the services are running

Zen runs as two systemd units - the Go server and nginx:

systemctl is-active zen.service nginx.service
active
active

The Zen server listens only on the loopback interface; nginx is the only service bound to public ports (:80 and :443):

sudo ss -tln | grep -E ':(80|443|8080) '
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      4096       127.0.0.1:8080      0.0.0.0:*

The web UI is served over HTTPS, and plain HTTP redirects to it:

curl -sk -o /dev/null -w 'https:// -> %{http_code}\n' https://127.0.0.1/
curl -s  -o /dev/null -w 'http://  -> %{http_code}\n' http://127.0.0.1/
https:// -> 200
http://  -> 301

Zen services, loopback + nginx binding, and HTTPS health on the VM

Step 5 - Read the per-VM administrator credential

On first boot the VM writes a root-only file with your Zen URL, the administrator email and the unique password generated for this VM:

sudo grep -E '^ZEN_URL=|^ZEN_ADMIN_EMAIL=' /root/zen-notes-credentials.txt
ZEN_URL=https://<vm-public-ip>/
ZEN_ADMIN_EMAIL=admin@zen.local

The ZEN_ADMIN_PASSWORD line in that file holds your unique password. Exactly one account is seeded, and only the per-VM password authenticates - a wrong or blank password is rejected:

EMAIL=$(sudo grep '^ZEN_ADMIN_EMAIL=' /root/zen-notes-credentials.txt | cut -d= -f2-)
PASS=$(sudo grep '^ZEN_ADMIN_PASSWORD=' /root/zen-notes-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'per-VM password -> HTTP %{http_code}\n' -X POST https://127.0.0.1/api/users/login \
  -H 'Content-Type: application/json' --data "{\"email\":\"$EMAIL\",\"password\":\"$PASS\"}"
curl -sk -o /dev/null -w 'wrong password  -> HTTP %{http_code}\n' -X POST https://127.0.0.1/api/users/login \
  -H 'Content-Type: application/json' --data "{\"email\":\"$EMAIL\",\"password\":\"wrong-nope\"}"
per-VM password -> HTTP 200
wrong password  -> HTTP 400

Secure by default: the per-VM info file, one seeded account, and a live login round-trip

Step 6 - First login

Open Zen in your browser (accept the self-signed certificate warning, or install a trusted certificate first - see below):

https://<vm-public-ip>/

Enter the administrator email (admin@zen.local) and the ZEN_ADMIN_PASSWORD from Step 5, then click Login. Open Settings from the sidebar to change the password immediately after your first sign-in.

Capturing notes

Click New to start a note. Zen uses Markdown, so headings, bold, lists, tables, task checklists and fenced code all render as you write. Add #tags inline to organise notes; tags let you filter without folders. Your notes appear in a timeline on the left, with full-text search across everything.

The Zen timeline with a rendered Markdown note - task list and code block

Markdown tables render inline, so structured notes stay readable:

A Zen note rendering a Markdown table

Task lists, fenced code blocks with syntax highlighting, tags and more all render from standard Markdown:

A Zen note rendering a task list and a code block

Step 7 - The single Go binary

Zen is a single self-contained binary that embeds the web UI and database migrations. A tiny zen-passhash helper is used only at first boot to seed the administrator password:

ls -la /usr/local/bin/zen /usr/local/bin/zen-passhash
file /usr/local/bin/zen | cut -d, -f1-3
-rwxr-xr-x 1 root root 16246024 /usr/local/bin/zen
-rwxr-xr-x 1 root root  2557168 /usr/local/bin/zen-passhash
/usr/local/bin/zen: ELF 64-bit LSB executable, x86-64

The single Zen Go binary and its loopback-bound service

The data disk

Zen's SQLite database (zen.db) and uploaded images live on a dedicated Azure data disk mounted at /var/lib/zen, independently resizable and kept separate from the OS disk:

df -h /var/lib/zen
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda         30G  240K   28G   1% /var/lib/zen

(The device letter can vary between boots - the mount is keyed by filesystem UUID in /etc/fstab, so /var/lib/zen is always the dedicated data disk.) To grow it, resize the data disk in the Azure portal, then extend the filesystem with sudo resize2fs against the device shown by lsblk. Snapshot the data disk in Azure to back up your notes.

Security updates

The image is captured fully patched (including Ubuntu phased updates) and unattended-upgrades remains enabled, so security patches keep flowing on your VM:

echo "held=[$(apt-mark showhold)]"; systemctl is-enabled unattended-upgrades.service
held=[]
enabled

Dedicated data disk and the OS security baseline

Enabling a trusted TLS certificate

The image ships a per-VM self-signed certificate so HTTPS works out of the box; browsers will warn until you install a trusted certificate. For production, point a DNS A record at the VM's public IP, ensure 443/tcp is open in the NSG, then install certbot and let it manage the nginx certificate. Replace the placeholders with your own domain and email:

sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example.com -m you@example.com --agree-tos

certbot configures the HTTPS server block and sets up automatic renewal. After it completes, Zen is available at https://your-domain.example.com/ with a trusted certificate.

Upgrading Zen

Zen is installed as a single Go binary at /usr/local/bin/zen. To upgrade within the 1.x line, build the new tag from source (make build in the upstream repository), replace the binary and restart zen.service. Always snapshot the data disk first, as the schema migrates on start. cloudimg support can assist with planning and performing upgrades.

Support

This image is backed by 24/7 cloudimg support covering deployment, upgrades, integrations, TLS termination and database administration. Contact us by email and chat.

Zen is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.