Tasks.md on Ubuntu 24.04 on Azure User Guide
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-mdsystemd 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.serviceas systemd units, enabled and active- An unauthenticated
/healthzendpoint on:80/:443for 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.

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.

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.

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.

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.

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.

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.

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, runsudo htpasswd -B /etc/nginx/.tasks-md.htpasswd adminand thensudo systemctl reload nginx. - TLS certificate: a per-VM self-signed certificate lives at
/etc/ssl/tasks-md/cert.pemand/etc/ssl/tasks-md/key.pem. For production, replace it with a CA-issued certificate (for example certbot with your own domain), thensudo systemctl reload nginx. - Loopback binding: the Node app is bound to
127.0.0.1:8080(server.jsis patched toapp.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-mdshows the Node app's output;journalctl -u nginxshows 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.