Tm
Applications Azure

Tasks.md on Ubuntu 24.04 on Azure User Guide

| Product: Tasks.md on Ubuntu 24.04 LTS on Azure

Overview

Tasks.md is a self-hosted, Markdown file-based task management board: a fast, minimalist kanban where every card is a plain .md file and every lane is a directory on the server's own filesystem. There is no database and no lock-in - your tasks are just Markdown you can read, edit, back up or keep under version control. The cloudimg image installs the pinned Tasks.md 3.3.0 release (SolidJS frontend + Koa/Node backend) built at install time, runs it under systemd, and locks it down for a marketplace appliance.

Because Tasks.md ships with no authentication of its own, an unmodified deployment would be open to anyone who can reach it. This image closes that gap: the Node app is bound to loopback only, and nginx does all customer-facing work on ports 80 and 443 behind a per-VM HTTP Basic Auth gate (user admin, unique password generated on first boot) with a per-VM self-signed TLS certificate. A small starter board ships preinstalled so the UI shows real cards on first sign-in. Backed by 24/7 cloudimg support.

What is included:

  • Tasks.md 3.3.0 built from the pinned upstream release and run as the tasks-md systemd service on Node.js 20 LTS
  • The Node app bound to loopback (127.0.0.1:8080) only, fronted by nginx on :80 (HTTP) and :443 (HTTPS)
  • Per-VM HTTP Basic Auth (user admin) protecting the board, with a unique password generated on first boot and stored only as a bcrypt hash
  • A per-VM self-signed TLS certificate so HTTPS works immediately on port 443
  • A starter board (lanes To Do, In Progress, Done with real Markdown cards) so the board and editor show real content on first login
  • tasks-md.service + nginx.service as systemd units, enabled and active
  • An unauthenticated /healthz endpoint on :80/:443 for Azure Load Balancer health probes
  • 24/7 cloudimg support

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 comfortable for a personal or small-team board; the app is trivially light. NSG inbound: allow 22/tcp from your management network, 80/tcp and/or 443/tcp for the board. The bundled TLS certificate is self-signed; for production, replace it with a CA-issued certificate (see Maintenance).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Tasks.md 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) and HTTPS (443). Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name tasks-md \
  --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 tasks-md --port 443 --priority 1010
az vm open-port --resource-group <your-rg> --name tasks-md --port 80  --priority 1020

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

systemctl is-active tasks-md.service nginx.service

Both report active. The Node app serves the board on the loopback address 127.0.0.1:8080 only; nginx fronts ports 80 and 443 with the per-VM HTTP Basic Auth gate, so the app is never exposed directly to the network.

The tasks-md and nginx services active, the Node.js runtime, the app listening on loopback 127.0.0.1:8080 only, and the unauthenticated health endpoint returning ok

Step 5 - Retrieve your web UI password

nginx protects the board with HTTP Basic Auth. The username is admin and a unique password is generated on the first boot of your VM and written to a root-only file:

sudo cat /root/tasks-md-credentials.txt

This file contains TASKS_MD_USERNAME, TASKS_MD_PASSWORD, and the TASKS_MD_URL to open in a browser. The password is stored on disk only as a bcrypt hash in /etc/nginx/.tasks-md.htpasswd, so no plaintext password ships in the image. Store the password somewhere safe.

The per-VM credentials file with the generated admin password, the bcrypt-only nginx Basic Auth entry, and the nginx version

Step 6 - Confirm the health endpoint

nginx serves an unauthenticated health endpoint for load balancers and probes:

curl -s http://localhost/healthz

It returns ok. This endpoint never requires authentication, so it is safe for an Azure Load Balancer health probe, while the board itself stays behind the password.

Step 7 - Confirm authentication and read the board via the API

Because a password is set on first boot, an unauthenticated request to the board returns HTTP 401, and common default credentials are rejected. The following reads the per-VM password from the credentials file, proves the auth round-trip over HTTP and HTTPS, and lists the starter board via the authenticated API:

PW=$(sudo grep '^TASKS_MD_PASSWORD=' /root/tasks-md-credentials.txt | cut -d= -f2-)
echo "no credentials     : $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
echo "default admin:admin: $(curl -s -o /dev/null -w '%{http_code}' -u admin:admin http://127.0.0.1/)"
echo "wrong password     : $(curl -s -o /dev/null -w '%{http_code}' -u admin:wrong-pw http://127.0.0.1/)"
echo "per-VM password    : $(curl -s -o /dev/null -w '%{http_code}' -u admin:$PW http://127.0.0.1/)"
echo "per-VM over HTTPS   : $(curl -sk -o /dev/null -w '%{http_code}' -u admin:$PW https://127.0.0.1/)"
curl -s -u admin:$PW http://127.0.0.1/_api/resource/ | python3 -m json.tool

It prints 401 for no credentials, the default admin:admin, and a wrong password, then 200 for the per-VM password over both HTTP and HTTPS. The API response lists the starter board's lanes and cards. Only the per-VM password reaches the board, because the Node app is bound to loopback and nginx is the only way in.

The HTTP Basic Auth round-trip returning 401 for no credentials, the default admin:admin and a wrong password, and 200 for the per-VM password over HTTP and HTTPS, plus the authenticated API listing the starter board's lanes and cards

Step 8 - Sign in and open your board

Browse to https://<vm-public-ip>/ (the certificate is self-signed, so your browser will warn on first visit - accept it, or install your own certificate as in Maintenance). Your browser prompts for a username and password: enter admin and the password from Step 5. Tasks.md opens on your board, showing the starter lanes To Do, In Progress and Done with their Markdown cards.

The Tasks.md kanban board after signing in, showing the To Do, In Progress and Done lanes with the starter Markdown cards

Step 9 - Open and edit a card

Click any card to open it in the Markdown editor. The editor gives you a full toolbar (headings, bold, lists, checklists, links, images, code blocks and tables), an Add tag control and a Due date picker. Everything you type is saved straight back to the card's .md file on disk.

The "Welcome to Tasks.md" card open in the Markdown editor, showing the editor toolbar and the card's real Markdown content

Step 10 - Track completed work

Checklists inside a card use standard Markdown task syntax (- [ ] and - [x]), and you can drag cards between lanes as work progresses. Here the Launch on Azure card in the Done lane shows a completed checklist.

The "Launch on Azure" card open, showing a completed Markdown checklist with all items checked

Step 11 - Your cards are Markdown files on disk

Everything you see on the board is a plain file on the server. Each lane is a directory under /var/lib/tasks-md/tasks and each card is a .md file inside it:

sudo find /var/lib/tasks-md/tasks -type f -name '*.md' | sort
sudo cat "/var/lib/tasks-md/tasks/To Do/Welcome to Tasks.md.md"

Because your board is just Markdown, you can back it up with a single directory copy, edit cards in any editor, or keep the whole board under version control.

The Markdown files that back the board on disk, and the content of the Welcome card, showing that every card is a plain .md file

Step 12 - Create your own lanes and cards

In the UI, use New lane in the top bar to add a lane, and the + button on a lane header to add a card. You can also create cards over the authenticated API - each call creates a directory or file on disk:

# create a lane (a directory) and a card (a .md file) via the API
PW=$(sudo grep '^TASKS_MD_PASSWORD=' /root/tasks-md-credentials.txt | cut -d= -f2-)
curl -u admin:$PW -X POST -H 'Content-Type: application/json' \
  -d '{"isFile":false}' https://<vm-public-ip>/_api/resource/Backlog -k
curl -u admin:$PW -X POST -H 'Content-Type: application/json' \
  -d '{"isFile":true,"content":"# My first card\n- [ ] do the thing"}' \
  https://<vm-public-ip>/_api/resource/Backlog/My%20first%20card.md -k

Maintenance

  • Password: the Basic Auth password is set on first boot and stored as a bcrypt entry in /etc/nginx/.tasks-md.htpasswd. To change it, run sudo htpasswd -B /etc/nginx/.tasks-md.htpasswd admin and then sudo systemctl reload nginx.
  • TLS certificate: a per-VM self-signed certificate lives at /etc/ssl/tasks-md/cert.pem and /etc/ssl/tasks-md/key.pem. For production, replace it with a CA-issued certificate (for example certbot with your own domain), then sudo systemctl reload nginx.
  • Loopback binding: the Node app is bound to 127.0.0.1:8080 (server.js is patched to app.listen(PORT, '127.0.0.1')), so nginx is the only path in. Keep it that way - do not change the bind address to a public interface.
  • Backups: your entire board is Markdown under /var/lib/tasks-md/tasks, and configuration (stylesheets, uploaded images, sort order) is under /var/lib/tasks-md/config. Back up or snapshot these directories like any production data.
  • Restrict access: restrict the board to trusted IP ranges in your Network Security Group in addition to the password.
  • Logs: journalctl -u tasks-md shows the Node app's output; journalctl -u nginx shows the web server's own logs.
  • Security patches: unattended-upgrades remains enabled so the OS continues to receive security updates automatically.

Support

cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.