Deployment guide

Tasks.md

Overview

Tasks.md is a self-hosted, Markdown file-based task management board. It is 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 Markdown files you can read, edit, back up or keep under version control.

This SecureAnyCloud image installs the pinned Tasks.md 3.3.0 release (SolidJS frontend and Koa/Node backend), built at install time and run under systemd. Tasks.md ships with no authentication of its own, so the image closes that gap. The Node app is bound to loopback only, and nginx serves ports 80 and 443 behind a per-VM HTTP Basic Auth gate with a per-VM self-signed TLS certificate. A small starter board is preinstalled, so the UI shows real cards on first sign-in.

What you get:

  • 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 with the lanes To Do, In Progress and Done and real Markdown cards, so the board and editor show real content on first login
  • tasks-md.service and nginx.service as enabled, active systemd units
  • An unauthenticated /healthz endpoint on :80 and :443 for Azure Load Balancer health probes
  • 24/7 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2s (2 vCPU / 4 GiB RAM) is comfortable for a personal or small-team board, because the app is very light.

Configure the network security group (NSG) to allow inbound 22/tcp from your management network, and 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 SecureAnyCloud, and select Create.

On the Basics tab, 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).

Select Review + create, then Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:tasks-md:default:latest
az vm create \
  --resource-group <your-rg> \
  --name tasks-md \
  --image sac1647283583153:tasks-md:default:latest \
  --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

Connect to the VM over SSH as the administrator user set in Step 2.

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

Check that both services are active:

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. 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

The file contains TASKS_MD_USERNAME, TASKS_MD_PASSWORD and the TASKS_MD_URL to open in a browser. On disk, the password is stored 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. Query it:

curl -s http://localhost/healthz

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

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

A password is set on first boot, so an unauthenticated request to the board returns HTTP 401 and common default credentials are rejected. The following commands read the per-VM password from the credentials file and prove the auth round-trip over HTTP and HTTPS. They then list the starter board through 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

The output shows 401 for no credentials, for the default admin:admin and for a wrong password. It shows 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 warns on the first visit. Accept the warning, or install your own certificate as described in Maintenance.

When 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 has 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]). Drag cards between lanes as work progresses. The Launch on Azure card in the Done lane shows a completed checklist.

Step 11 - Your cards are Markdown files on disk

Everything 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 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, select 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 lanes and cards through 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, 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 run 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. Do not change the bind address to a public interface.
  • Backups. Your entire board is Markdown under /var/lib/tasks-md/tasks. 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. In addition to the password, restrict the board to trusted IP ranges in your NSG.
  • Logs. journalctl -u tasks-md shows the Node app's output, and 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

SecureAnyCloud provides 24/7 expert support for this image. For questions or help with your deployment, contact sales@secureanycloud.com.

Deploy on Azure

Launch Tasks.md on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure